Managing State in React using Unstated-Next

Avatar of Kingsley Silas
Kingsley Silas on (Updated on )

In a previous post, we saw how to manage state using Unstated. As you might recall, Unstated uses React’s built-in setState to allow you create components that can consume state by subscribing to a provider — like the React’s Context API.

Well, we’re going to build off that last post by looking at Unstated Next, a library that author Jamie Kyle identifies as the “spiritual successor” to his Unstated project. Unstated Next provides both React Hooks and the Context API for managing state. Unstated was a minimal abstraction to the idea of React Hooks before they were a fully-fledged thing. But now that Hooks in React are so good, that abstraction is unnecessary and Unstated Next simply incorporates them while providing an API to share state and logic with the Hooks.

We’re going to specifically look at how to manage state in both single and multiple components using Unstated Next. It might be helpful to check out the previous post on Unstated prior to moving ahead, but it’s not totally necessary.

Example: A minimal form component

To start off, we’ll create a tiny React application for a form that merely contains a text input for a person’s name and a button to submit it. When the button is clicked, we’ll display the name as a paragraph element above the form. The source code for this example is available on GitHub.

This is going to be a Bootstrap React application which we can spin up by using Create React App. Let’s install that and then change directories into the project folder.

npx create-react-app unstated-next-form
cd unstated-next-form>

We need to add Unstated Next as a dependency:

## yarn
yarn add unstated-next

## npm
npm install --save unstated-next

We’ll be making use of React Hooks and createContainer from Unstated Next, so let’s import those into the App component:

// src/App.js
import React, { useState } from 'react';
import { createContainer } from "unstated-next";

Next, we will create a custom hook. We’ll have our state in it, which we can create using useState:

// src/App.js
// ...same as before

const useForm = () => {
  const [input, setValue] = useState("");
  const [name, setName] = useState("Barney Stinson");

  const handleInput = event => {
    setValue(event.target.value);
  };

  const updateName = event => {
    event.preventDefault();
    setName(input);
    setValue("");
  };

  return {
    input,
    name,
    handleInput,
    updateName,
  };
};

We have two states defined here. input will be used for keeping track of values entered into the text input and it will be updated using the handleInput method. name will be updated when the button is clicked, which will trigger the updateName method.

OK, now we can create a container by passing our custom hook as a parameter to the createContainer() method.

// src/App.js
// ...same as before

const FormContainer = createContainer(useForm);

This will create a container which we can use across our application. Yeah, you read that right, but let’s take one step at a time. We’re starting with this one component to see how it works with Unstated Next.

Now, let’s create a Form component that looks like this.

// src/App.js
// ...same as before

const Form = () => {
  const form = FormContainer.useContainer();
  return (
    <div>
      <p>Hello! {form.name}</p>
      <div>
        <input
          type="text"
          value={form.input}
          onChange={form.handleInput}
        />
        <button onClick={form.updateName}>Save</button>
      </div>
    </div>
  );
};

We’re assigning the variable form to the value obtained from calling FormContainer.useContainer(). The value contains the states and methods defined in the custom hook we created above. With that, we can make use of the state and methods provided — but for that to happen, we have to wrap the Form component in a provider.

const App = () => (
  <Form.Provider>
    <Form />
  </Form.Provider>
)

With what you have learned so far, try building a minimal to-do application using Unstated Next. If you get stuck, feel free to check this repository to see how I made mine.

Example: Sharing state across multiple components

OK, so you got a hint earlier that we can use our form container anywhere we’d like. One of the benefits of using Unstated Next is that it makes it possible to share state across multiple components. To see how this works, we’ll build a tiny app that uses the form features we made above and also makes it possible to create to-do tasks using the same state. The name of the user can be updated in the form component, and this update will also be reflected in the to-do component. Two birds of a feather!

There’s a repo for this example as well, so feel free to clone or download it as we plow ahead.

Let’s spin up a new project and install the necessary dependencies:

npx create-react-app unstated-next-app
cd unstated-next-app
yarn unstated-next shortid

The state for the application will live in a separate file. We want to have the states for the form and to-do components in the store, and the methods needed for updating them too. Create a store.js file inside the src directory and make it look like this;

// src/store.js
import { useState } from "react";
import shortid from "shortid"
import { createContainer } from 'unstated-next'
export const useStore = () => {
  // Construct a list that contains two default tasks
  const list = [
    { id: 1, title: 'Write code' },
    { id: 2, title: 'Buy milk' }
  ]
  const [input, setValue] = useState("");
  // Let's set a legen -- wait for it -- dary default name that updates on form submit
  const [name, setName] = useState("Barney Stinson");
  const [todos, addTodo] = useState(list);
  const [item, setTodo] = useState("");
  const handleInput = event => {
    setValue(event.target.value);
  };
  const updateName = event => {
    event.preventDefault();
    setName(input);
    setValue("");
  };
  const handleTodo = event => {
    setTodo(event.target.value);
  };
  const handleSubmit = event => {
    event.preventDefault();
    const value = {
      id: shortid.generate(),
      title: item
    }
    addTodo(todos.concat(value));
    setTodo("");
  };
  return {
    input,
    name,
    handleInput,
    updateName,
    todos,
    item,
    handleTodo,
    handleSubmit
  };
}
export const StoreContainer = createContainer(useStore)

We make use of useState() to create the states we need. The methods are defined and all of this happens inside the custom hook, useStore(). We create the StoreContainer and then pass useStore() as a parameter to createContainer(). With that, we can make use of the StoreContainer in the necessary components where want to make use of the state and methods we have defined.

Starting with the form section, create a file called form.js and it should look like what I have below;

// src/form.js
import React from "react";
import { StoreContainer} from "./store";

const FormComponent = () => {
  const form = StoreContainer.useContainer();
  return (
    <div>
      <p>Hello! {form.name}</p>
      <div>
        <input type="text" value={form.input} onChange={form.handleInput} />
        <button onClick={form.updateName}>Change Name</button>
      </div>
    </div>
  );
};
export default FormComponent;

We’re using StoreContainer to access the state and methods we need. We’ll do the same thing for the task component which you can create in a todo.js file.

// src/todo.js
import React from "react";
import { StoreContainer } from "./store";

const TodoComponent = () => {
  const todo = StoreContainer.useContainer();
  return (
    <div>
      <p>Add Todos</p>
      <input type="text" value={todo.item} onChange={todo.handleTodo} />
      <button onClick={todo.handleSubmit}>Add</button>
      <div>
        <p>Dear {todo.name}, here are your current tasks;</p>
        {todo.todos.map((item) => {
          return (
            <ul key={item.id}>
              <li>{item.title}</li>
            </ul>
          );
        })}
      </div>
    </div>
  );
};
export default TodoComponent;

You can see that todo.name can only be updated in the FormComponent. That’s because we need a way to provide the state in both components. That’s why we’re going to turn again to Provider and add one in the App component just like we did in the previous example.

import React from 'react';
import TodoComponent from "./todo";
import FormComponent from "./form";
import { StoreContainer } from "./store"

function App() {
  return (
    <div className="App">
      <StoreContainer.Provider>
        <FormContainer />
        <TodoContainer />
      </StoreContainer.Provider>
    </div>
  );
}
export default App;

There we go! By adding the provider, data can be taken from the form component, stored in the provider, and passed back to the task list. 💥