Managing state in React with Unstated

Unstated is a new easy-to-use state management solution for React.

State management in JavaScript applications has always been a developer pain point. React is no exception. The simplest way to manage state in react is its built-in component state API - setState. But it is often insufficient in handling the requirements of complex applications. That's where different solutions come in - Flux was one of the first, then came Redux and swept everyone off their feet. Another big one is MobX. But these solutions have their own ways of doing things, their own concepts and best practices that you need to wrap your head around before using them. If you are tired of all the tedious setup and boilerplate code needed to work with libraries like Redux, then Unstated might be the solution you have been looking for.

Unstated is very simple and easy to understand if you are already comfortable with React and its ways. It builds upon existing React concepts like setState, class structure, context and common patterns like render props. Its API is extremely simple and small as it provides only three things - a Container class, a Subscribe component and a Provider Component.

We will learn how to use Unstated by creating a "Language Filter" app (because I think no one wants another of those counter or todo apps). It is a very simple app consisting of two main parts - 

1. Adding and removing words that need to be filtered.

2. Filtering the sentence that the user inputs.

The app we will build is hosted on Codesandbox and Github. Feel free to fork it and play around.

Note: This article assumes the reader has experience using React and ES6+ code.

Getting Started

We will use Codesandbox to bootstrap a new React project right in the browser. We will create two directories under src - components for all the react components and containers for the unstated state containers. We will use Spectre CSS library to make our small app look presentable.

Container

In Unstated, we manage state by extending the Container class. Extending and using this Container class to build a state container is very similar to extending React.Component for a component class, with the exception of the render method.

We will start by creating two Container classes - 

1. SentenceContainer - for managing the sentence that is given and the filtered version of the sentence.

2. WordlistContainer - for managing the adding and removing of words that need to be filtered.

/* ./containers/SentenceContainer.js */

import { Container } from 'unstated';

class SentenceContainer extends Container {

state = {

filteredSentence: '',

};


filterSentence = wordlist => e => {

const filteredSentence = e.target.value

.trim()

.split(' ')

.map(word => {

if (wordlist.includes(word.toLowerCase())) {

return '*'.repeat(word.length);

}

return word;

})

.join(' ');

this.setState({ filteredSentence });

};

}


export default SentenceContainer;

In this, we import and extend the Container class. As you can see, it is pretty similar to a React class component without the render method. We first initialize the state with a single property filteredSentence with the default value of an empty string ''. And we also have a method that will use the wordlist passed to it to filter the given sentence. We are using the experimental public field class syntax (a.k.a the arrow function binding shenanigans). In the method, we are utilizing closure to hold the wordlist array that contains all the banned words. Then, we grab the sentence value from the e.target object passed to us from the event handler and we check the sentence for banned words. If any such words are there, we replace them with * (asterisks). Lastly, we use setState (works pretty much the same way as React's setState) to update the state.

/* ./containers/WordlistContainer.js */


import { Container } from 'unstated';


class WordlistContainer extends Container {

constructor(...words) {

super();

this.state = {

words: words,

};

}


addWord = word => {

const wordToAdd = word.toLowerCase();

this.setState(({ words }) => {

if (!words.includes(wordToAdd)) {

const newWords = [...words, wordToAdd];

return { words: newWords };

} else {

return null;

}

});

};


removeWord = word => () => {

this.setState(({ words }) => {

if (words.includes(word)) {

const wordIndex = words.findIndex(w => w === word);

const newWords = [

...words.slice(0, wordIndex),

...words.slice(wordIndex + 1),

];

return { words: newWords };

} else {

return null;

}

});

};

}


let wordList = new WordlistContainer('shit', 'damn');

export default wordList;

Again, we extend the Container class but this time we use the constructor to pass in the initial state we want. We again have two methods - 

1. addWord - it accepts a word parameter and adds it to the words array in the state if it does not already exist in the array. This time we are using the callback function form of setState call. The return value is used to update the state, if null is returned nothing is updated (hence, causes no rerender). 

2. removeWord - it is quite similar to addWord but it removes the word instead of adding it.

One crucial change to notice is, in case of the previous container we exported out the class itself. But for this one, we created an instance and exported that specific instance out. Even though we used this just for passing some optional defaults, it can also be used for interesting things like dependency injection (when passed to inject prop on Provider component).

Subscriber

We have created the state and the logic to manage the state already. Now, we must use the state to render something for our user. In unstated, the  Subscriber component uses the render props pattern to expose the state data and methods contained within a container to a react component.

Let's get started on two react components that are named complimentarily to their respective containers.

/* ./components/Wordlist.js */

import React, { Component } from 'react';

import { Subscribe } from 'unstated';


import wordList from '../containers/WordlistContainer';

class Wordlist extends Component {

wordInput = React.createRef();


handleClick = addWord => () => {

if (this.wordInput.current.value) {

addWord(this.wordInput.current.value);

this.wordInput.current.value = '';

}

};


render() {

return (

<Subscribe to={[wordList]}>

{({ state: { words }, addWord, removeWord }) => (

<div className="container">

<h5>Banned Words</h5>

<div className="wordlist">

{words.map(word => (

<Word key={word} word={word} removeWord={removeWord} />

))}

</div>

<div className="input-group words-form">

<input

type="text"

ref={this.wordInput}

className="form-input word-input"

name="word"

/>

<button

className="btn btn-primary add"

onClick={this.handleClick(addWord)}

>

Add

</button>

</div>

</div>

)}

</Subscribe>

);

}

}

const Word = ({ word, removeWord }) => (

<span className="chip word">

{word}

<button

href="#"

className="btn btn-clear"

aria-label="Remove"

onClick={removeWord(word)}

/>

</span>

);


export default Wordlist;

We create a Wordlist component that uses the Subscribe component from unstated. Subscribe component has a to prop that accepts an array value of all the containers that need to be subscribed to. It then passes the respective instances to the render prop function as parameters. 

Inside the component, we create a ref (using the new React.createRef API) and a handleClick method to read the text value of the input and pass it to the addWord method we get from the wordList container instance. We use the words array on the state object to render the banned words and also attach the removeWord method to each of the Word components' onClick event.

/* ./components/Sentence.js */


import React from 'react';

import { Subscribe } from 'unstated';


import wordList from '../containers/WordlistContainer';

import SentenceContainer from '../containers/SentenceContainer';


const Sentence = () => (

<Subscribe to={[wordList, SentenceContainer]}>

{(

{ state: { words } },

{ state: { filteredSentence }, filterSentence },

) => (

<div className="container">

<h5>Filtered Sentence</h5>

<pre className="code filtered-sentence">{filteredSentence}</pre>

<div className="form sentence-form">

<input

className="form-input"

onChange={filterSentence(words)}

type="text"

name="sentence"

placeholder="Enter sentence here..."

/>

</div>

</div>

)}

</Subscribe>

);


export default Sentence;

In the Sentence component, we subscribe to not one but two container instances, including the wordList instance we already subscribed to in the previous React component. Also, the parameters passed to the render props callback are in the order in which we pass the containers in the to array prop of the Subscriber component.

Inside this component, we use the words array from the wordList state to pass as parameter to the filterSentence method from Sentence container to update the filterSentence which in turn is used to render the filtered sentence. This is a good example of how to use state and methods from different containers coherently.

Provider

Last but not the least, we need to nest all the components using Subscribe under the Provider component. Provider is needed to keep track of all the container instances internally.

/* ./index.js */

import React from 'react';

import ReactDOM from 'react-dom';

import { Provider } from 'unstated';


import Sentence from ./components/Sentence';

import Wordlist from './components/Wordlist';


import './styles.css';

function App() {

return (

<div className="app">

<h1>Unstated Language Filter</h1>

<Sentence />

<div className="divider" />

<Wordlist />

</div>

);

}


const rootElement = document.getElementById('root');

ReactDOM.render(

<Provider>

<App />

</Provider>,

rootElement,

);

In the index file, we create the App component and render Sentence and Wordlist components as its children. Finally, wrap the App component in the Provider Component and use ReactDOM.render to mount the app on the DOM element with id root.

Conclusion

Unstated is one of the most elegant state management solutions out there. So, next time when you find yourself reaching out for libraries like Redux or MobX give Unstated a try instead. It's fairly easy to get started with and works perfectly for a lot of use cases.