Managing State in React With Unstated

0
26

As your application becomes more complex, the management of state can become tedious. A component’s state is meant to be self-contained, which makes sharing state across multiple components a headache. Redux is usually the go-to library to manage state in React, however, depending on how complex your application is, you might not need Redux.

Unstated is an alternative that provides you with the functionality to manage state across multiple components with a Container class and Provider and Subscribe components. Let’s see Unstated in action by creating a simple counter and then look at a more advanced to-do application.

Using Unstated to Create a Counter

The code for the counter we’re making is available on GitHub:

View Repo

You can add Unstated to your application with Yarn:

yarn add unstated
Container

The container extends Unstated’s Container class. It is to be used only for state management. This is where the initial state will be initialized and the call to setState() will happen.

import { Container } from ‘unstated’

class CounterContainer extends Container {
state = {
count: 0
}

increment = () => {
this.setState({ count: this.state.count + 1 })
}

decrement = () => {
this.setState({ count: this.state.count – 1 })
}
}

export default CounterContainer

So far, we’ve defined the Container (CounterContainer), set its starting state for count at the number zero and defined methods for adding and subtracting to the component’s state in increments and decrements of one.

You might be wondering why we haven’t imported React at this point. There is no need to import it into the Container since we will not be rendering JSX at all.

Events emitters will be used in order to call setState() and cause the components to re-render. The components that will make use of this container will have to subscribe to it.

Subscribe

The Subscribe component is used to plug the state into the components that need it. From here, we will be able to call the increment and decrement methods, which will update the state of the application and cause the subscribed component to re-render with the correct count. These methods will be triggered by a couple of buttons that contain events listeners to add or subtract to the count, respectively.

import React from ‘react’
import { Subscribe } from ‘unstated’

import CounterContainer from ‘./containers/counter’

const Counter = () => {
return (
<Subscribe to={[CounterContainer]}>
{counterContainer => (
<div>
<div>
// The current count value
Count: { counterContainer.state.count }
</div>
// This button will add to the count
<button onClick={counterContainer.increment}>Increment</button>
// This button will subtract from the count
<button onClick={counterContainer.decrement}>Decrement</button>
</div>
)}
</Subscribe>
)
}

export default Counter

The Subscribe component is given the CounterContainer in the form of an array to its to prop. This means that the Subscribe component can subscribe to more than one container, and all of the containers are passed to the to prop of the Subscribe component in an array.

The counterContainer is a function that receives an instance of each container the Subscribe component subscribes to.

With that, we can now access the state and the methods made available in the container.

Provider

We’ll make use of the Provider component to store the container instances and allow the children to subscribe to it.

import React, { Component } from ‘react’;
import { Provider } from ‘unstated’

import Counter from ‘./Counter’

class App extends Component {
render() {
return (
<Provider>
<Counter />
</Provider>
);
}
}

export default App;

With this, the Counter component can make use of our counterContainer.

Unstated allows you to make use of all the functionality that React’s setState() provides. For example, if we want to increment the previous state by one three times with one click, we can pass a function to setState() like this:

incrementBy3 = () => {
this.setState((prevState) => ({ count: prevState.count + 1 }))
this.setState((prevState) => ({ count: prevState.count + 1 }))
this.setState((prevState) => ({ count: prevState.count + 1 }))
}

The idea is that the setState() still works like it does, but this time with the ability to keep the state contained in a Container class. It becomes easy to spread the state to only the components that need it.

Let’s Make a To-Do Application!

This is a slightly more advanced use of Unstated. Two components will subscribe to the container, which will manage all of the state, and the methods for updating the state. Again, the code is available on Github:

View Repo

The container will look like this:

import { Container } from ‘unstated’

class TodoContainer extends Container {
state = {
todos: [
‘Mess around with unstated’,
‘Start dance class’
],
todo: ”
};

handleDeleteTodo = (todo) => {
this.setState({
todos: this.state.todos.filter(c => c !== todo)
})
}

handleInputChange = (event) => {
const todo = event.target.value
this.setState({ todo });
};

handleAddTodo = (event) => {
event.preventDefault()
this.setState(({todos}) => ({
todos: todos.concat(this.state.todo)
}))
this.setState({ todo: ” });
}

}

export default TodoContainer

The container has an initial todos state which is an array with two items in it. To add to-do items, we have a todo state set to an empty string.

We’re going to need a CreateTodo component that will subscribe to the container. Each time a value is entered, the onChange event will trigger then fire the handleInputChange() method we have in the container. Clicking the submit button will trigger handleAddTodo(). The handleDeleteTodo() method receives a to-do and filters out the to-do that matches the one passed to it.

import React from ‘react’
import { Subscribe } from ‘unstated’

import TodoContainer from ‘./containers/todoContainer’

const CreateTodo = () => {
return (
<div>
<Subscribe to={[TodoContainer]}>
{todos =>
<div>
<form onSubmit={todos.handleAddTodo}>
<input
type=”text”
value={todos.state.todo}
onChange={todos.handleInputChange}
/>
<button>Submit</button>
</form>
</div>
}
</Subscribe>
</div>
);
}

export default CreateTodo

When a new to-do is added, the todos state made available in the container is updated. The list of todos is pulled from the container to the Todos component, by subscribing the component to the container.

import React from ‘react’;
import { Subscribe } from ‘unstated’;

import TodoContainer from ‘./containers/todoContainer’

const Todos = () => (
<ul>
<Subscribe to={[TodoContainer]}>
{todos =>
todos.state.todos.map(todo => (
<li key={todo}>
{todo} <button onClick={() => todos.handleDeleteTodo(todo)}>X</button>
</li>
))
}
</Subscribe>
</ul>
);

export default Todos

This component loops through the array of to-dos available in the container and renders them in a list.

Finally, we need to wrap the components that subscribe to the container in a provider like we did in the case of the counter. We do this in our App.js file exactly like we did in the counter example:

import React, { Component } from ‘react’;
import { Provider } from ‘unstated’

import CreateTodo from ‘./CreateTodo’
import Todos from ‘./Todos’

class App extends Component {
render() {
return (
<Provider>
<CreateTodo />
<Todos />
</Provider>
);
}
}

export default App;

Wrapping Up

There are different ways of managing state in React depending on the complexity of your application and Unstated is a handy library that can make it easier. It’s worth reiterating the point that Redux, while awesome, is not always the best tool for the job, even though we often grab for it in these types of cases. Hopefully you now feel like you have a new tool in your belt.