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.
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.
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:
There are a couple of state management concepts that we will be referring to in this post:
Use your package manager of choice to install sweet-state:
npm i react-sweet-state
# or
yarn add react-sweet-state
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
};
To create a store, we will need to call createStore
.
import { createStore } from 'react-sweet-state';
export const Store = createStore({
initialState,
actions: {},
name: 'TasksStore'
});
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.
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:
export const nameSelector = (state) => state.listName;
After we've created our selector, we can turn it into a hook with createHook
:
import { createStore, createHook } from 'react-sweet-state';
export const Store = createStore({
initialState,
actions: {},
name: 'TasksStore'
});
export const useName = createHook(Store, { selector: nameSelector});
Now in our UI, we can use that hook to grab the name value from our store:
import { useName } from '../../state/store';
const Name = () => {
const [listName] = useName(); return <NameView name={listName} />;
};
export default Name;
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:
const updateListName = (listName) => ({ setState }) => {
setState({ listName });
}
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');
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 })
This is because when you call setState
, it will do a shallow merge with what is currently in your store, like this:
{ ...state, listName }
(This is the same as how React's setState
function works).
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:
const updateTaskName = (id, name) => ({ getState, setState }) => {
const { tasks } = getState(); const updatedTask = { ...tasks[id], name };
setState({ tasks: { ...tasks, [id]: updatedTask } });
};
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
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.
After you've defined these actions, make sure you go back to your store and add them:
import { createStore } from 'react-sweet-state';
export const Store = createStore({
initialState,
actions: { updateListName, ...taskActions }, name: 'TasksStore'
});
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');
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
});
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();
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);
And then wrap it around each to-do list:
<TodoListContainer scope={id}>
{ /** To-do list code here */ }
</TodoListContainer>
By passing in a scope
prop, any actions or selectors used inside of that container will be scoped to the 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} />
You can then access this data from your actions:
const updateTaskName = (id, name) => (
{ getState, setState },
{ userPreferences }) => {
// ...
};
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.