DEV Community

loading...
Cover image for React state management with sweet-state

React state management with sweet-state

Emma Goto πŸ™
Front-end developer. In my spare time I like to make Trello Power-Ups ✨
Originally published at emgoto.com on ・5 min read

Looking for a simpler state management solution for React? This post will walk you through how to get set up with react-sweet-state.

Check out the react-sweet-state example. It's a to-do list app I'll be referring to throughout this post.

Why use sweet-state?

sweet-state was created to meet the state management needs of Jira, a project-tracking software. I’d recommend checking out Alberto’s post on the creation of react-sweet-state for more of its backstory.

One of its benefits is that you don’t need to wrap your app in a provider. This works well for a codebase like Jira’s, which consists of many small apps with their own state. When you need to share state between two apps, it’s easier when you don’t need to wrap your entire codebase in a provider.

Redux vs sweet-state

You might be wondering how sweet-state stacks up against Redux, the most widely-used state management library.

The two main differences between Redux and sweet-state are:

  1. sweet-state need you to wrap the app in a provider, and Redux does.
  2. Redux has separate actions and reducers. sweet-state merges these two together.

Getting up to speed: stores, actions and selectors

There are a couple of state management concepts that we will be referring to in this post:

  • A store is a central location where we put all the state for our app.
  • An action is in charge of modifying the store. We dispatch these actions from the UI.
  • A selector returns a specific chunk of our store to the UI.

"sweet-state diagram: actions modify the store, and selectors get data from the store"

Installing react-sweet-state

Use your package manager of choice to install sweet-state:

npm i react-sweet-state
# or 
yarn add react-sweet-state
Enter fullscreen mode Exit fullscreen mode

Creating your sweet-state store

The example app we will be using is a to-do list app. The shape of its store will look like this:

const initialState = {
  listName: 'My new list', // <- the name of our to-do list
  tasks: {} // <- all our to-do list items live inside this object
};
Enter fullscreen mode Exit fullscreen mode

To create a store, we will need to call createStore.

// state/store/index.js
import { createStore } from 'react-sweet-state';

export const Store = createStore({
    initialState,
    actions: {},
    name: 'TasksStore'
});
Enter fullscreen mode Exit fullscreen mode

Fun fact: we can use Redux Devtools when debugging sweet-state!
The name we choose for our store is used to identify it in there.

Creating a selector in sweet-state

Since we store all our data in our sweet-state store, we also need selectors to get that data out of the store for our UI to use.

For example, if we wanted to get our to-do list's name:

// state/selectors/index.js
export const nameSelector = (state) => state.listName;
Enter fullscreen mode Exit fullscreen mode

After we've created our selector, we can turn it into a hook with createHook:

// state/store/index.js
import { createStore, createHook } from 'react-sweet-state';

export const Store = createStore({
    initialState,
    actions: {},
    name: 'TasksStore'
});

export const useName = createHook(Store, {
    selector: nameSelector
});
Enter fullscreen mode Exit fullscreen mode

Now in our UI, we can use that hook to grab the name value from our store:

// components/name/index.js
import { useName } from '../../state/store';

const Name = () => {
    const [listName] = useName();
    return <NameView name={listName} />;
};

export default Name;
Enter fullscreen mode Exit fullscreen mode

Creating an action in sweet-state

When a user modifies the name of the to-do list, we will dispatch an action to modify the name value in our store.

Here's the action:

// state/actions/list-name/index.js
const updateListName = (listName) => ({ setState }) => {
    setState({ listName });
}
Enter fullscreen mode Exit fullscreen mode

Understanding setState

You’ll notice that the action is a function that returns another function.

However, when we call this action from our UI, we will only need to call the outer function:

updateListName('New name');
Enter fullscreen mode Exit fullscreen mode

Behind the scenes, sweet-state will handle providing the setState parameter.

You may have also noticed that our store contains both tasks and listName, but we only passed in listName:

setState({ listName })
Enter fullscreen mode Exit fullscreen mode

This is because when you call setState, it will do a shallow merge with what is currently in your store, like this:

{ ...state, listName }
Enter fullscreen mode Exit fullscreen mode

(This is the same as how React's setState function works).

Using getState

As well as setState, sweet-state also provides a getState function.

If we needed to modify a specific task in our to-do list, we can use getState to first grab all the tasks. Then we can find the one that we want to modify:

// state/actions/tasks/index.js
const updateTaskName = (id, name) => ({ getState, setState }) => {
    const { tasks } = getState();
    const updatedTask = { ...tasks[id], name };
    setState({ tasks: { ...tasks, [id]: updatedTask } });
};
Enter fullscreen mode Exit fullscreen mode

When setting state, you have to make sure to never directly modify the state object that you receive. This means we can’t do this:

const { tasks } = getState();
tasks[id].name = name
Enter fullscreen mode Exit fullscreen mode

We want our app to re-render when values in our store change, but if we directly edit the state object this won’t happen.

If you're dealing with complex state objects, you can also check out libraries like immer to make life easier.

Adding actions to your store

After you've defined these actions, make sure you go back to your store and add them:

// state/store/index.js
import { createStore } from 'react-sweet-state';

export const Store = createStore({
    initialState,
    actions: {
        updateListName,
        ...taskActions
    },
    name: 'TasksStore'
});
Enter fullscreen mode Exit fullscreen mode

Accessing actions using hooks

Like our selector, we'll want to access our actions via hooks.
Any selector hooks we create already provide you with all your actions, so you don't need to do anything.

We can use it by accessing the second item returned in our hook's array.

const [listName, { updateListName }] = useName();

updateListName('new name');
Enter fullscreen mode Exit fullscreen mode

Creating a hook without a selector

You may have a scenario where your component only needs to access some actions, and doesn’t need any data. In this case, you can pass in a null selector to createHook:

export const useTaskActions = createHook(Store, {
    selector: null
});
Enter fullscreen mode Exit fullscreen mode

When you make use of this new hook, make sure to ignore the first element in the array (as it will be null):

const [, { updateTaskName, deleteTask }] = useTaskActions();
Enter fullscreen mode Exit fullscreen mode

Create multiple stores using containers

In our current example, we have one store that contains our to-do list. But what if we had two to-do lists, and wanted two stores?

In this scenario, we can make use of containers to scope our data:

import { createContainer } from 'react-sweet-state';

export const TodoListContainer = createContainer(Store);
Enter fullscreen mode Exit fullscreen mode

And then wrap it around each to-do list:

<TodoListContainer scope={id}>
    { /** To-do list code here */ }
</TodoListContainer>
Enter fullscreen mode Exit fullscreen mode

By passing in a scope prop, any actions or selectors used inside of that container will be scoped to the container.

Passing in props into a container

You can also use containers to pass in props that then become available in your actions.

If your to-do list has some user preferences:

<TodoListContainer userPreferences={userPreferences} />
Enter fullscreen mode Exit fullscreen mode

You can then access this data from your actions:

const updateTaskName = (id, name) => (
    { getState, setState },
    { userPreferences }
) => {
   // ...
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

With a store, selectors, and actions, you'll be all set up to do state management with sweet-state!

For more advanced use-cases, I encourage you to check out the sweet-state docs.

Discussion (4)

Collapse
vaibhavkhulbe profile image
Vaibhav Khulbe

Looks like there are a lot of options for state management with React!

Collapse
emma profile image
Emma Goto πŸ™ Author

Yeah! There's at least two more I want to cover (Mobx and Recoil)

Collapse
hangindev profile image
Jason Leung πŸ§—β€β™‚οΈπŸ‘¨β€πŸ’»

Thanks for the introduction, Emma! Never heard of it before. Will keep it in mind for future projects. 🍬

Collapse
emma profile image
Emma Goto πŸ™ Author

No worries! Thanks for reading!