Transcript
Zelinschi: I heard that there's a big, talented community here in London and a lot of you love JavaScript so I thought, "Yes, why not talk a little bit about state management," which is one of the most interesting problems that we have to do and face in client side applications, especially with the whole things shifting a lot towards the client side, state management is often referred to as the root of all evil in JavaScript applications. We're going to talk about this, we're going to talk about React a lot.
Redux
A lot of people are using Redux and they're pairing it with React to solve the state management issues. This is great, Redux is very interesting and it's a great tool, it has a lot of advantages, it's been around for a couple of years. It's been battle tested in real applications by real developers. They're in production, the overall ecosystem had time to mature. The core team that is the core contributors team is extremely solid, they're doing an awesome job maintaining it. The ecosystem around it, it's booming, we have Redux Thunk, we have Sagas, we have Redux DevTools which is amazing and I'm pretty sure you using it if you're using Redux. The overall ecosystem is also great, there are a lot of strong points to Redux, I've been using it for quite some time and I've always been using it pretty much and I would say that this is also the de facto why I used it as a client side cash and as a way to sync data between different components, which are not necessarily in a parent/child relationship and it's great at doing that.
Alongside with it, there are some things that you buy into by default. Opinionated, and I sustain this and I know it's my thing, but I feel that at times, Redux brings in a lot of the boilerplate. You have maps data props, you have mapDispatchToProps, you have a couple of things. You have a decent API but then you have an entire middleware chain system that you can or cannot opt into but you get the structure for that by default. You can use it or you cannot but it's there by default.
I was thinking, "Yes, there are some very good points to this," but because of the ways I was using it, can I somehow build Redux or a small percentage of it, not the whole thing, just a small percentage of it, can I just build it with plain old React? Can we do that? Not the entire functionality but just the subset that will cover 80% of the use cases. Maybe you've heard of the Pareto principle, the 80/20 rule, if we can somehow build something very simple that will cover 80% of the use cases of Redux on the client side, maybe it's worth the shot.
React has been on the on hype lately and they're doing amazing work in terms of development. Some of the things, some of the new APIs that have been available since React 16 are the Context and Hooks APIs. Context has been around for almost like a year, it was released at the end of March 2018. It's basically an overhaul of the older API and that's awesome. Hooks, however, something that got people and developer completely hooked on React. This has been around for a couple of months, started late last year and now have been shipped in February in the latest React 16.8 and they're stable.
This is what we're going to use, Context is just a way to pass or to make information easily available between different components and Hooks, they're a way to share state for logic between components because React did not have something like that. Everything was pretty blunt when it came to sharing state for logic between different components.
Coding
We're going to use those to build something that resembles a state management library, a simple approach. Let me show you what I've got so far which is basically going to be our starting point. I have a project here, it's pretty simple, this is the index file, we just have two different components. Don't look at the styling and everything, it was just put together very quickly and the styling is not the purpose but we have an app component and a component that's called "AnotherComponent" because that's how original I can be with naming but they're not in a parent/child relationship. Let's look very quickly into the AnotherComponent file, what this does is, it just renders a paragraph. Very simple, we're going to see where this is going to take us. The app component, this is a little bit more complicated but not too much. We have a static array of users, nothing very complicated and what it basically does is, it renders a form with an input, a label and input and a button and then it displays the list of those users by using this particular component which is called List. List doesn't do that much but it just receives an array of information and a display attribute that it can use. Let's go back to App.js and let me show you how this looks, just a very simple UI. We have key users, everything is hard coded. This is what we want to do, we would like to get into a place where we have information, this information is not hard coded, we have it pulled from the Context.
We have the ability or we would like to build the ability to put data into that Context, into that particular shared global store that we're going to use. We would like the ability to display it here which is in the App component and here where we don't have nothing, we currently don't have anything at this point which is the AnotherComponent. That's to show the sharing of the state between different components. Let's see how we could do that.
Think about the API first, this is a good idea always. We would like to use Context from React and that comes up with the idea of a provider, we want to use providers. This particular Fragment component, I would like to replace it with something that would be called an AppProvider. Now, we don't have that but let's imagine we would. I don't want anything to put into this AppProvider components, I wanted to have everything sorted out on its own. I would create it here, I would call it "AppProvider," and I would use some sort of function to create it, let's call it "createProvider." Now, for every particular provider, we need to pass on like an initial state, that's going to be used in the actual component in the value of the Context, basically. Here I'm going to have like some sort of initial state, we don't know what that is.
Where would we get this from? I have a file here that's called "simpply" and this is where we're going to code our things. I'll just import React, I'm going to quickly create a Context, so we're going to call it "React.createContext," and here we're going to have that createProvider function. This will receive an initial state as a parameter, we can initialize that with an empty object as well just by default and it has to return another component, it has to return the provider component. This will be a React component, it will have children imported by default or this structure and this props and this will have to return like the Context, the provider will pass in the value and the value here, let's call it the State, will be the initial state that we pass in as a parameter here. Then this will have to render children as well.
Let's say we export this, we export createProvider and we export Context for now, this is not necessarily a good practice but we're going to come back to this later, so this is just the first step. We go back in index.js, we can import the createProvider from simpply and instead of this initial state, let's come up with something. Doesn't really matter, let's do it on the spot.
For example, we're going to have an array of users, let's just do something like “new Array”, put five users in, we're going to fill them with 0. By the way, if you don't do this and you try to map over the array, it doesn't work, been there, very interesting book. Then we're going to map for every user, we're just going to return something that has the name property and we're going to say, "user," and then, "index + 1" and I'm going to put "index" here as well. This will generate an array of users with the names, User 1, User 2, User 3, and so on and so forth. Then, how do we display this into components?
Let's go back to App.js, we can remove the mock users, we have the provider and it's globally surrounding our application but now we need to take that Context in into this particular components. I'm going to import Context from simpply, and we're going to use here, the values that we have in Context. We're going to use here, this entire object that we're going to get under the name state, because this initial state maps to this particular object that we embedded here. I can close this, I can close List for sure.
Do you know any Hook that would allow us to use the Context in the consumer phase? It's called useContext, we can import that. It's very simple and here, we're going to say, "Ok, I'm taking the state from useContext and I'm passing in the context that I have imported and then I'm also destructuring state and I'm pulling in the users. Now here, I can take the users and I can pass them to the List and let's see if we get something. We get the generated users from 1 to 5 from the context right now.
We've done the first step, we have data that's in context and that we're pulling in into a component but it's not very much, we can't really change the data at this point. Let's look at how we could use this particular input field to put data back into the context. Now, if I type into the input field, nothing happens and that's because there are no listeners added and stuff like that. Let's add the ability to have data shown into the input as we type. In order to do that, we need to set the state of that particular input on and on again and for that, we're going to use what hook? useState. We're going to say we have the user and we have the ability to set user, hooks return this pair, they return an array with the value that you're going to change and a function to change that.
Just be careful that the set value, so the second, the function that sets the value of that particular data does not do the merge like the set state function does. We're going to say useState with the initial value of empty and this has to be a const because otherwise it's not going to work. Here, we're going to go to this and we're saying the value is user and here, we're going to do setUser and we're going to pass target.value. Now, if we go back into the browser, I should be able to type stuff. This works, we've solved this issue. If we go back, nothing happens when we click the button, if we console log something here, we should be able to see the user.
We have the value over here of whatever I'm typing. Here, I'd like to add stuff in the Context. We need to add things and then we need to re-render all of the components that are using that Context in the consumer mode. How do you re-render something in React? How do you make sure that some value updates in the UI? You need to trigger a set state somewhere, that's pretty much the deal, we need to trigger a set state and we're going to do that in the provider that is wrapping all of our components.
Let's think a little bit about the API, maybe I could do something like "setGlobalValue" here I want to set the users value to this particular array. We're just going to use an empty one for now. I don't know from where I'm going to get it, but when I call this function, I want to have this working. Let's go back here and say, "You know what? We need to create some sort of state and we need to update it afterwards." In order to do that, we're just going to use that useState hook and then we're going to say, "Ok, we're going to have an application state and we're going to have a function to set the app state.”
This will basically be done with the useState hook and this is going to use what as a parameter here? The initial state, the one that I'm passing in on as the value in the index for the provider. I have to return this like this, "return." Now, I need a different function, I need a function to pass it into the Context and then in two particular components so I can change this. We're going to create this setGlobalValue function, we said that in terms of API, it could take a key for whatever value you want to update in the state and the value for that particular key and what's it going to do? Well, it's going to set the AppState. How do you do that?
I was telling you that there's no merge, so then you need to always set the AppState. Also like in SetState, you can pass in a function so you can have a prevState here and you can return an object with prevState destructured. Then for that particular key that you passed in, you can pass in the value of the setGlobal function. Now, I'm going to pass it in here and make it available in the context for all particular components. I need to use AppState as the new state of my application because, otherwise, I'm just going to stick to the initial state forever and ever and nothing is going to change.
Let's go back here, now we can import this setGlobalValue function from context, let's see if we can get something working over here. If I click the "Add" button, I'm now setting the global users array to an empty one. We don't want an empty one, actually, we can say something like - let's call it "const newUsers" and this is basically going to be a destructuring of the users array and then I'm going to add this new object which will have as the name key, will have the user particular value in the state. If we do something like this and now I type some "ddd" here, then I get it added in. Not only that, but let's go and connect another component to this as well.
I'm just going to take the list over here, I'm going to put it here, I'm going to uncommon this, and I go back and I paste these two things, just to use the same code, useContext. We don't need the setGlobalValue at this point and yes, we do need to import context as well in another component. Let's see if something happens right now and we should see in both places and yes, it works. We're going to take this even a step further, because there are some things missing at this point. Do you remember the Connect API in Redux? Connect API is something very interesting because at this point, our App component pulls in the entire state. Maybe it's not interested in the entire state, maybe it's just users or maybe it's just some other random particular data.
We want to provide components with some sort of API that's similar to Connect, that only pulls a slice of the state into this component and that component can not know about anything else, it's not going to be their business. If we remember, Connect in Redux, Connect had an API of something like this, it had a mapStateToProps function, it had a mapDispatchToProps function, and then everything was applied to a certain component. These two are functions, the execution of this basically returns another function that gets applied to that component. "Component."
This execution over here return some sort of EnhancedComponent that knows how to connect to the global context, it knows how to pull specific data from that into the component and it does all bunch of optimizations behind the scenes. Let's create something that's similar, we don't necessarily need the mapDispatchToProps and I'm going to come back to this in a second. We do need some sort of functionality to tell the component, "Hey, it's not all the state that you need to get in, it's just some slice of it," and we're going to use this mapStateToProps function to do that.
We need to create the Connect functionality, it's a function that takes in a function as a parameter and returns another function that takes a component as a parameter, which is going to return this EnhancedComponent. I know it sounds a little bit scary but we're going to do it, trust me. Let's go back here and create this particular function, so we're going to say "const connect,". This will take "mapStateToProps." It's going to be a function, then it will return a function that, as we said, it's going to take a React component as a parameter and this will return some sort of EnhancedComponent.
The EnhanceComponent needs to do some things, it needs to connect to the store and pull only the right data and pass it down as props. Let's create this EnhancedComponent. This is going to obviously have some props that we want to pass down and what do we want to return here? We want to return actually the component that we're having passed in as a parameter component but then we want to pass it some props. We want to pass the props that are in this EnhancedComponent, we want to pass it the destructuring of the mapStateToProps that is applied to some sort of a state, we'll see where that comes from.
This is basically going to return us only a part of the global object and we want to spread that, as props to the underlying component and we want to play the setGlobalValue as well to have it available. This particular component does not have neither state nor setGlobalValue. What we need to do here is we need to have the Context being pulled in, so we're going to say "const," we're going to say "state" and "setGlobalValue," and we're going to use context. This particular component, we need to import useContext over here and we're going to use it here. We're going to pull in the overall state, we're going to pull setGlobalValue as a function that you can pass on to change or alter that state and the mapStateToProps is going to take only a slice of that state and pass it down to the EnhancedComponent.
Now, we don't necessarily need to export the context, we can export the Connect function. If we go back in App.js, we don't need to use context anymore here, we don't need to use this because they're going to be passed in as props, let's connect our component to this. We're going to say "const," we need to define a mapStateToProps, and this will receive the overall state, the global state as the parameter, and it will return only a slice of it. What are we interested in this component? Well, we only interested in the users, I'm going to pull just the users from the state and that's it.
Instead of just exporting the App, I'm just going to export the Connect with mapStateToProps applied to this particular app. Then I'm going to have users injected as props so I'm going to have users here and I'm going to have setGlobalValue as well here. I don't need to use Context anymore and we still have the new users, we still have the setGlobalValue. This should be something that should work, so let's see. "ctx is not exported from simpply", because we need to do this in another component as well, so we're going to connect this. I can take this particular functionality here, put it at the end. Now, we're going to use another component which are end users and setGlobalValue, they're going to be passed in as props. Something works, hooks can only be called.
I'm perfectly prepared for something like this, watch this, this is how you develop JavaScript. I'm going to discard old changes, I'm going to go on to the Connect branch and it's going to magically work. I was using useContext, I should have used it in the EnhancedComponent, not outside of it. This is where we have to have the useContext. Extra of what I've done here is that the component that we pass in as parameter, I've also applied memorization to it by using React.memo, which is basically the functional equivalent of fewer component.
We don't want this component to trigger the different algorithm every single time the EnhancedComponent changes because probably the props or the mapStateToProps result is not going to change so this is just the slide optimization. Other than that, this is the same, mapStateToProps here is the same, we have users setGlobalValue passed in. Now, we have a particular functionality, Connect. We're not pulling all the data and I'm going to show you a different example in a second and this thing still works.
There are some small drawbacks with this approach. Just to come back a little bit, so at this point, setGlobalValue, it's an interesting function but it doesn't really dispatch actions and there is no way to have a trace of that over time. You don't really know which function changed to what and from what component, there is no logging of it. If we remember Redux, that's something that Redux with DevTools does pretty well so there are ways that we can take this even further. Plus, we're calling this set global function as a global value function but React puts at our disposal another Hook which is called useReducer which returns a dispatch function and you can use that to dispatch to actions. This, plus the fact that you can do many more optimizations in terms of performance and some re-rendering stuff, I decided to put all these ideas into a small package that is called simpply because it's simple and it's about state management and it's simple and I'm not very original and good with names.
Simpply is out there, it's open source, and I wanted to share some of those ideas with you guys. Let me show it to you how it looks and then I can show you another example that uses simpply and it's a little bit more complex than what we've done so far. I was saying that we're using Context but this time, we're no longer going to use that setGlobalValue function, we're just going to use the useReducer hook and for that, we need some sort of a reducer function.
Down here, there's a usage of this particular hook called "useReducer," you pass it in a reducer function which has state and action and you pass in some sort of globalInitialState. We're going to see how that is getting created. This allows you to dispatch actions that have type and payload, so it returns you the entire state and a dispatch function. You can inject this dispatch function as the value. Instead of the setGlobalValue, you can inject the dispatch function and make it available to various components. Not only that, I've done here a couple of optimizations, the React.memo, I was telling you about that before, and there is also an interesting thing with useMemo hook which is also another memorization thing.
What I'm saying here is that if I'm in development mode, I'm using the reference to keep track of the previous state before I'm applying a certain action. Instead of just dispatching the action, I'm overriding the dispatch method with the same dispatch from useReducer plus a smaller console log, just to see that something has been triggered in terms of action. I would get some sort of message like, "Hey, yes, you've triggered this action” and then this is the previous state and this is the state after the change. It's just a very basic logging functionality, it's definitely not at the same level with Redux DevTools and everything but just to have something that can show you what the track of the changes in your system is.
useMemo allows me to compute this patch function just once because, otherwise, it would always get overwritten and it would be a new reference every single time, which would trigger re-renders, so it has some small optimizations baked in. Let me show you how I'm using this, this is a project that uses simpply. It's very simple, it has the same idea of users being split into two different components, pretty much something that we've implemented ourselves right now in live coding but it also has like a list of Chuck Norris jokes that is being pulled from the server because I wanted to demonstrate how this works within the context of client server communication and because everybody loves Chuck Norris jokes, hopefully.
This part over here is being pulled from an API and I'm going to show you in a second how that works. Here, you can see some ideas about that logging thing that I was telling you. It tells you the action that has been triggered, SET JOKES is something that triggers after the fetch from the API is done and you pull all the jokes about Chuck Norris into the context, into the global store. Then you have an idea about, "Yes, this is the previous state and I have no jokes available and now this is the current state and I have an array of interesting stuff about Chuck Norris. If I clear this and if we add stuff, we're going to see the ADD_USER particular action that was triggered and, again, the previous state and the current state.
Let's look very high level of how this is implemented. If you're familiar with the idea of reducers and then how they combine with combineReducer in Redux, in simpply, there's this notion of storage entity and you can think of it conceptually as the equivalent of an entity in database modeling. I made a special folder for that, I have "users" and "jokes." The "index" file just combined them together by using this functionality called createSystemStorage. It's the equivalent of createReducer but it does some things a little bit different because simpply is very opinionated. It's convention-based which allows us to not write a lot of boilerplate code because it's based on some conventions.
I'm combining "users" and "jokes" and let's look at users very simply. We have two things being exported from a storage entity file, an initialState which is the equivalent of what we've coded together and a list of effects. This is just a map, where a certain action name maps to a certain function and this function is not the reducer, per se, it just takes some sort of state and it takes a payload and it returns the new state of the system for this particular resource, only for users. If I want to add a user, I'm just going to destructure the entire state that was before and I'm going to add the payload extra. How does that look, for example, if we go into App?
Here, instead of setGlobalValue, I'm dispatching an action with the type ADD_USER, that's going to map to the ADD_USER key for this effect, it's going to call this function. The payload here, this particular object over here will actually be the payload that I'm going to add extra to the array. It's a little less convoluted than Redux because it's more opinionated and it allows us to do some conventions and to bypass some code. About the jokes, also, let's have a look at another component because this is the one that takes users and jokes from the global store and this is how we're pulling jokes from this particular URL.
We're using the use effect hook, we're just calling it once on component now because we have no array of parameters to watch. Truth be told, I don't write code like this, always rapid within a try-catch block but I totally forgot to do that, so don't say that I told you to not write try-catch blocks, definitely, you need to have that. We're using Async/Await which I love when it comes to pulling data or client server communication and I feel that the whole flow of Async/Await pairs very nicely with the synchronous dispatch. We're waiting for the data and then once that's done, we can dispatch SET_JOKES.
You can imagine we could do something like here, we're going to SET_LOADER and we're going to say "true," and then this here, it will be "SET_LOADER" and it will be "false." This is how you do an entire flow of "Hey, set this particular value loader to true," which can trigger a spinner or something in the UI, then you await for the data. After the data comes back, you dispatch, you put the jokes into the global store, then you close the loader, basically, you set it back to false. This is something that could work out as a float. I'm going to switch back to slides.
simpply
Here, you're going to have links, I'm going to put these on Twitter, you're going to have links to the open source repository, to the example that uses simpply, it's going to be here and to the thing that we live coded together. I just want to close off with some thoughts about this and maybe some very immediate future ideas. simpply is just that, it's just very simple, it definitely doesn't have the breath of Redux and it doesn't cover all the use cases. It was just about this idea of taking a small portion portion of Redux which was like almost 80% of the use cases I was using in Redux and just extracting that into something that's very easy to manage.
The surface API is small, it's easy to understand, it doesn't have a lot of depth to it and it pairs very nicely with Async/Await flows for fetching data. Of course, it's not at that level, it doesn't have any community around it or at least yet. It doesn't have any middleware chain system, it doesn't have any ecosystem. You've seen the logging functionality, that's pretty basic, the Redux DevTools allows you to inspect the state, it gives you nice graphs about it, you can even copy paste tests for your reducers, so that's pretty cool but it's not its goal.
It's just not trying to serve that, I think it's always a battle when you try to do something in terms of open source/libraries/utilities/framework, whatever. It's always a battle between how much do you provide in terms of API and to balance that nicely with the actual scope and purpose of what you're trying to achieve because there's always this risk of not keeping things in balance. Just as a future immediate thought, maybe all these extra functionalities that they're not there, I was thinking of some sort of a plugin system, something that's very similar to what Babel is doing or JSLint or other or Webpack with plugins and stuff.
That would be interesting to have stuff that are opt in, you can bring them and use them as plugins pretty much the same that Redux does with its middleware chain and that other functionalities connect to that, like, Redux-saga or Redux Thunk, they're all middlewares and you can use them if you want into your application. That could be something that could be the next steps.
That's pretty much what I had in mind, I want to close off with one last thing which I totally believe in. I love surrounding myself with ever smiling people and I really think that you're the most beautiful people when you do smile. Redux is going to work great and simpply is going to work great and React is going to work great and View is going to work great and JavaScript is going to be amazing if you do this. If not, it's going to be a crappy language and stuff like that. Do smile, people, because you're the most beautiful when you do.
Questions and Answers
Moderator: If you're building an MVP that you know is going to turn into a large system in about 12 months' time, do you still start with simpply?
Zelinschi: This is up and running for less than a month, it was just an idea. I have it open and running in a project that at this point is pretty much four months old. The code base, of course, is not huge, it's being split between five people working on this and we didn't really have any scaling issues so far, I didn't have this experience. I would totally use it for small and mid-sized projects but more than that, I don't have the practical experience yet of using it. I can only say that in four months and five developers, this still works and it keeps the flow going.
Participant 1: Thank you for the great talk, I love the new Hook API and I really want to play with it some more myself. My question was, going back to your first example about the app and Another app component, if you have, for instance, the state of all the users inside the Another app component, does the Another app component gets withdrawn if the list of users changes?
Zelinschi: If you have an extra state on top of that or it just being pulled from the context?
Participant 1: No, if AnotherComponent is completely autonomous, it has its own state or whatever, and then the state of the list of users is inside a different component inside AnotherComponent. I was wondering if the AnotherComponent also gets withdrawn because it's in the same context provider?
Zelinschi: By default, any connected component to the provider, so any consumer type of component from the provider consumer pattern in context gets automatically withdrawn when the value changes, when the value that you’re passing into the provider changes. Let's look at "simpply-talk." Whenever something here changes all the components that are being connected to either Connect or have the useContext hooks, they're automatically going to withdraw. There are ways to bypass that and that's what I did here in terms of small performance improvements with React.memo, because this EnhancedComponent is going to get withdrawn but this one, you don't need it to, for example, if jokes change but the users don't. Then I use React.memo because if jokes change and users don't, then the props, they're going to be the same, the mapStateToProps is going to be the same, this is going to be the same provided that you don't mess up the function reference.
You're going to get some sort of performance by not withdrawing the child but all the connected components with Context, they're going to get automatically withdrawn. It doesn't even matter if they're in PureComponent or something that has ShouldComponentUpdate set to "false," it's forcing that withdrawn behind the scene.
Participant 2: Thanks for your great talk, I do understand that you might be conscious on the time and you only show us regarding what's happening with one Context but are you supporting as well the multiple Context? Zelinschi: By default, I've tried this in a separate project, when I say separate project, I mean a separate thing to try it out, not a project, per se, for a client. It's not on GitHub but yes, it does work by default, I haven't done anything special, the idea of nesting Context is something that is available by default in the Context API but you're going to have more multiple createContexts functions and you can use the createStore functionality to create different Contexts with different storage entities behind. As long as you pass them down correctly between components, you're not going to have an issue.
Participant 3: I was wondering what your thoughts were on TypeScript combining with React.
Zelinschi: I've been using TypeScript with React in the last two or three projects I've been doing and I absolutely love it. I will be honest and say that initially I had some sort of, "Do I need this? Is it like too much overhead?" I might know some JavaScript to get around it but it's awesome, especially if the teams are distributed, if multiple people are working on the same code base if the experience level varies inside of the team and it even helped me in so many ways to not shoot myself in the foot.
I absolutely love it, I think it's definitely a great tool to pair your JavaScript with and I know there are a lot of pushes and advancements to have types by default support in the language so maybe at that point, TypeScript is not going to be that relevant but we're not there yet. Yes, I would totally advise everyone to use that.
See more presentations with transcripts