Published on Oct 16, 2018. 9 minute read.
Below are the accompanying notes I wrote that acted as a rough speaking guide, and may or may not have been spoken during the event itself.
This presentation assumes that you are using at least version 16.3 of React JS, this is because the refreshed version of Context was not released until this version.
Please ask questions as we go along as others may find the answers helpful. Should your question require a long time to answer, I may defer the answer until after the presentation is over. (Come speak to me afterwards).
At a basic level, state is what allows you to create components that are dynamic and interactive. State enables users to interact with your application. Without state, your application would do little. You will almost always want your application to respond to changes in data, usually when fetched from an external API. State is the enabler here.
Local state is used at component level and has a limited use-case. Local state is available when defining an ES6 class which extends
React.Component, and is defined using an instance property called
state can not be mutated directly, but can be updated by calling an instance method called
setState. More on this later.
Context API was revised in React 16.3 and gives us a Provider/Consumer pattern that enables us to pass state down our component hierarchy. You define a provider that effectively wraps local state, and makes that state (as well as update functions) available to any child component at any depth in the hierarchy via a Consumer object. More on this later.
There are other state management tools available, such as MobX and Unstated. There is a comprehensive list on Hackernoon, The React State Museum.
One does not simply choose a state management tool.
It is important to know your tools, know your project, understand the future direction of the project, and make informed decisions accordingly.
There is no reason why you can’t use local state, Context API & Redux all in the same application, although I wouldn’t start with that.
You probably wouldn’t want to mix multiple advanced state management tools like Redux, MobX and Unstated in a single React application, because that would exponenetially increase complexity and the learning curve for devs joining your team or helping maintain your codebases.
Local state works best with forms. When local state is used frequently it becomes unwieldly, and hard to reason about, and most importantly, harder to unit test. I like to use local state when handling data that isn’t final yet, i.e. data that will eventually be posted to the server once the user has finished inputting it.
Context API, thanks to its hierarchical nature, is particularly useful for enabling access to data from child components. Historically, if we had some data defined in a parent component that we need to access in a child component, we would pass that data down via
props. This would often go several levels deep. This was messy and made code more noisey and harder to reason about. Context API exposes a Consumer, which has access to the state, eliminating prop drilling. We will see a couple of demos shortly.
Redux is strikingly similar to Context API in that it effectively has a Provider/Consumer pattern, although its less obvious. The main difference is that instead of having potentially many different states, there is a single state contained within a global store, that can be accessed using a function called
connect, which can be used to access the state and pass that state through to the component. Redux is built on top of the Context API, which can be clearly seen when looking at the
Provider component in the
react-redux Github repository. Link.
Notes of interest:
- Container/presentation (Parent/child) relationship
- Presentation component takes a default state, which it binds to the form
- Presentation component uses local state to store users input
- Once form is submitted, data is lifted up to the parent, which does something useful with it (displays an
alertin this case)
- User can reset form using the reset button. This works because we do not (cannot) mutate the initial state, we copy it to our local state
- Each time the user makes a change to a field, we call
setStateto apply that change to our local copy of state.
onSubmittedfunction is passed from the container to the presentation component
The form state is isolated in the presentation component until the user finalises it (submits the form), when it is lifted up and processed accordingly.
setState is asychronous, batched and can cause the component to re-render.
This is a fairly contrived example where we are passing an initial state object to the registration form. What happens, if, after the form has been rendered the first time, the initialState object gets changed, causing a re-rendering?
Answer: The registration form will not be reflect the changes, because the local state never gets updated. We used to have a component lifecycle method called
ComponentWillReceiveProps, which would pass you the local state and the updated props so you could update your component accordingly, but this lifecycle method has recently been deprecated and will be removed entirely from React in the next major version release.
Notes of interest:
- The demo shows a component whos styling can be toggled with a button. There is a
toggleThemefunction that lives in the
Appcomponent that calls
setStatewith the updated theme.
- When the component renders, the
currentThemeis retrieved from state and passed to the
Baconcomponents, which utilise it.
currentThemeobjects are passed down to the toggle button.
- This code is messy, even with only 1-2 levels of depth. Imagine the pain over several levels!
Notes of interest:
- No longer necessary to pass props down to each individual component
ThemeProviderobject has been inserted into the component hierarchy
- Each component is responsible for fetching what they need from the state using a
ThemeProvideris a component, which wraps a
- Returns a
ThemeContext.Providerobject, which contains only the children components within, and the exact value to provide to each consumer
- The consumer is exported as-is
- Use the
Buttoncomponent as an example of how this is syntactically fiddly, and what this code would look like without ES6 arrow functions or destructuring (arguably, easier to reconcile).
Context API is really just a wrapper around
This can result in tidier code because you don’t have calls to
setState all over the place (that is encapsulated). No need to write
this.state.myProp either, just
myProp, which reduces noise. This pattern is easy to conceptualise and reason about, making it approachable to developers of all levels.
I have a few complaints. Almost all examples will show usages of the Conumer with ES6 arrow functions and destructuring. Also because of JSX we have to throw in an extra set of curly braces. This makes it syntactically fiddly, even with editor tools such as ‘Indent Rainbow’ and ‘Bracket Pair Colorizer’ I still find myself awkwardly trying to get my brackets to match up.
The Enzyme unit test framework from AirBnB currently has poor support for Context API. This will likely change in future releases.
Redux is slightly different in that it has a root level
Provider that is applied across the entire application. You use actions and reducers to update state, and a function called
connect to retrieve state from the store.
Notes of interest:
- There is no
Providerdirectly in our
Providerwraps everything and is usually used when calling
- There is some setup required to define your reducers and actions
Baconcomponent has two exports. The first is the plain stateless functional component that works exactly the same as all the other demos. The second export (default export) has the component wrapped with
connect, which takes two callback functions as its parameters, which returns a function that then takes the component as its arguments. The first function,
mapStateToProps, gets passed the state from the store, and allows whatever properties are required to be extracted and returns. This return value then gets passed to the component to be consumed.
Redux has a steeper learning curve because of the need for actions, a store, reducers, dispatchers, and the immutable nature.
Redux scales very well when working with many components and even applications running on the same page.
Redux has excellent tooling (Redux Developer Tools) that make debugging a breeze. The debugger allows you to step through time and see what changes were made to a piece of data and what action caused that change.
As mentioned, unit testing is easy because there is no mocking required, we just test our plain function and pass it whatever arguments are needed.
It is important to know what tools are available and what problem they are trying to solve, then use the most appropriate tool for the job.
Local state for anything other than forms is a bad idea.
When working with local state, prefer a container/presentation relationship, and lift state up when ready. We’ve not mentioned
ref for good reason, thats because 99% of the time you shouldn’t need to use it!
Don’t pay too much attention when sensationalist news says that some tool is dead. Redux is not dead, is being actively maintained and developed, and is widely used.
My recommendation is to use Context API when it makes sense to do so going forward, then should a problem arise that only Redux can solve, then add it to your project on demand.
Please avoid prop drilling, it makes code hard to reason about and tidious to follow through.
This space is still evolving. My favourite up-and-coming technology is GraphQL, developed by Facebook, and Apollo, created by Meteor. Apollo has state management built in and may be game changer, there are early signs that GraphQL with Apollo will gain mainstream adoption over the next couple of years, so watch this space. Thats a whole presentation on its own.