Written by John Au-Yeung✏️
There are a few ways to share data between React components. First, we can pass data from parent to child via props. React also has the context API to pass data between components with any relationship as long as we wrap the context provider component inside the React components that we want to share data between.
We also have global state management solutions like Redux andMobX which let us share data easily within the entire app.
Any component that wants to get the latest value of a state can subscribe to a data store with a global state management solution.
Another state management solution is Kea, which works similarly to Redux. We can subscribe to a store created with Kea to get data and set the latest state. Kea is powered by Redux, so lots of concepts like reducers and stores will be used with Kea as well.
In this article, we’ll look at how to use Kea in a React app as a global state management solution.
Basic state management
We can get started by creating an app with create -react-app by running:
npx create-react-app kea-app
Then we can install the libraries needed by Kea, which is Kea itself, Redux, and React-Redux. To install them we run the following code:
npm i kea redux react-redux reselect
Then we can write a simple app with Kea as our app-wide global state management solution by writing the following code:
//index.js
import React from "react";
import ReactDOM from "react-dom";
import { resetContext, getContext } from "kea";
import { Provider } from "react-redux";
resetContext({
createStore: {},
plugins: []
});
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={getContext().store}>
<App />
</Provider>,
rootElement
);
//App.js
import React from "react";
import { kea, useActions, useValues } from "kea";
const logic = kea({
actions: () => ({
setName: name => ({ name })
}),
reducers: ({ actions }) => ({
name: [
"",
{
[actions.setName]: (_, payload) => payload.name
}
]
})
});
const Name = () => {
const { name } = useValues(logic);
return <p>{name}</p>;
};
export default function App() {
const { setName } = useActions(logic);
return (
<div className="App">
<input onChange={e => setName(e.target.value)} />
<Name />
</div>
);
}
In the code above, we imported the React Redux’s Provider
component and then wrapped it around our whole app to let Kea work as the app-wide state management library.
However, we pass in getContext().store
as the value of the store instead of a Redux store as we usually do. We leave the createStore
and plugins
properties with an empty object and array in the object that we pass into resetContext
since we aren’t using any plugins and isn’t changing any options when we create the store.
Then in App.js
, we create an object with the kea
function which has the logic that we’ll use in our store. It included logic for both retrieving and setting values for our store.
We have the following in App.js
to create the logic
object that we’ll use to read and write values from the store:
const logic = kea({
actions: () => ({
setName: name => ({ name })
}),
reducers: ({ actions }) => ({
name: [
"",
{
[actions.setName]: (_, payload) => payload.name
}
]
})
});
We have the actions
property with the methods that we’ll use to set the value of the name
state in the store. The reducers
property has the action name as the key of the object.
The first entry of the reducer array is the default value of it.
It uses the name of the function as the identifier for the reducer function that we have in the object of the second entry of the array of the reducer. Like a Redux reducer, we return the value that we want to set in the store with the reducer function.
Then we set the name
value in the store by calling the Kea’s useActions
function with the logic
object passed in. It has the setName
method that we can call with the object that it returns.
In the input element of App
, we call setName
to set the value of name
to the inputted value.
Then in the Name
component, we called Kea’s useValues
method with the logic
object that we created earlier as the argument and then get the name
value from the store and render it.
Therefore then the text that’s typed into the input will show in the Name
component below it.
Listeners
Listeners are functions that run after an action is dispatched. They’re useful if we want to be able to cancel these actions that are within listeners.
To use it, we can add the kea-listeners
package by running:
npm i kea-listeners
We can use it to listen to an action that’s being performed by Kea and then use that to trigger another action as follows:
//index.js
import React from "react";
import ReactDOM from "react-dom";
import { resetContext, getContext } from "kea";
import { Provider } from "react-redux";
import listeners from "kea-listeners";
import App from "./App";
resetContext({
createStore: {},
plugins: [listeners]
});
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={getContext().store}>
<App />
</Provider>,
rootElement
);
//App.js
import React from "react";
import { kea, useActions, useValues } from "kea";
const logic = kea({
actions: () => ({
setCount: count => ({ count }),
setDoubleCount: doubleCount => ({ doubleCount })
}),
listeners: ({ actions, values, store, sharedListeners }) => ({
[actions.setCount]: ({ count }) => {
actions.setDoubleCount(count * 2);
}
}),
reducers: ({ actions }) => ({
count: [
0,
{
[actions.setCount]: (_, payload) => payload.count
}
],
doubleCount: [
0,
{
[actions.setDoubleCount]: (_, payload) => payload.doubleCount
}
]
})
});
const Count = () => {
const { count, doubleCount } = useValues(logic);
return (
<p>
{count} {doubleCount}
</p>
);
};
export default function App() {
const { count } = useValues(logic);
const { setCount } = useActions(logic);
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>Increment</button>
<Count />
</div>
);
In the code above, we added the listeners
plugin by adding the listeners
plugin to the array that we set as the value of the plugins
property in index.js
.
Then we can listen to the actions.setCount
action as it’s being run in the listeners
property. The listeners
property is set to an object that takes an object with the actions
, values
, store
, and sharedListeners
properties.
In the example above, we called the setDoubleCount
action by accessing the action method with the actions
property.
We also defined the doubleCount
reducer so that we can call the setDoubleCount
action, as we did above, to update the value of the doubleCount
state. Then in the Count
component, we call useValues
with logic
to get both count
and doubleCount
and display the values.
Therefore, when we click the Increment button, we get one count that increments by 1, which is count
, and another one that increments by 2, which is doubleCount
.
Canceling actions
We can add a breakpoint
method call, which returns a promise to wait for a specified number of milliseconds where we can cancel the action if the same action is called again.
For instance, we can write the following code to create a cancellable action:
//App.js
import React from "react";
import { kea, useActions, useValues } from "kea";
const logic = kea({
actions: () => ({
setName: name => ({ name }),
setResult: result => ({ result })
}),
listeners: ({ actions, values, store, sharedListeners }) => ({
[actions.setName]: async ({ name }, breakpoint) => {
await breakpoint(3000);
const res = await fetch(`https://api.agify.io?name=${name}
`);
breakpoint();
actions.setResult(await res.json());
}
}),
reducers: ({ actions }) => ({
name: [
"",
{
[actions.setName]: (_, payload) => payload.name
}
],
result: [
"",
{
[actions.setResult]: (_, payload) => payload.result
}
]
})
});
export default function App() {
const { result } = useValues(logic);
const { setName } = useActions(logic);
return (
<div className="App">
<input onChange={e => setName(e.target.value)} />
<button onClick={() => setName("")}>Cancel</button>
<p>{result.name}</p>
</div>
);
}
In the code above, we have the method with the actions.setName
key that’s set to an async
function and takes a breakpoint
function. We call the breakpoint
function with 3000 milliseconds of waiting to let us cancel the request.
We also have a cancel button which also calls the setName
action, which lets us cancel the action. The second breakpoint call breaks cancel the action when the setName
action is called a second time.
Sagas
To incorporate sagas into Kea, we have to install the Redux-Saga and Kea Saga packages by running:
npm install --save kea-saga redux-saga
Then we can add sagas and use them with Kea as follows:
//index.js
import React from "react";
import ReactDOM from "react-dom";
import { resetContext, getContext } from "kea";
import { Provider } from "react-redux";
import sagaPlugin from "kea-saga";
import App from "./App";
resetContext({
createStore: true,
plugins: [sagaPlugin({ useLegacyUnboundActions: false })]
});
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={getContext().store}>
<App />
</Provider>,
rootElement
);
In the code above, we added the sagaPlugin
from kea-saga
as our Kea plugin. We also have to set createStore
to true
to let us use sagas in our store:
//App.js
import React from "react";
import { kea, useActions, useValues } from "kea";
import { put } from "redux-saga/effects";
const logic = kea({
actions: () => ({
setCount: count => ({ count }),
setDoubleCount: doubleCount => ({ doubleCount })
}),
start: function*() {
console.log(this);
},
stop: function*() {},
takeEvery: ({ actions }) => ({
[actions.setCount]: function*({ payload: { count } }) {
yield put(this.actions.setDoubleCount(count * 2));
}
}),
reducers: ({ actions }) => ({
count: [
0,
{
[actions.setCount]: (_, payload) => payload.count
}
],
doubleCount: [
0,
{
[actions.setDoubleCount]: (_, payload) => payload.doubleCount
}
]
})
});
const Count = () => {
const { count, doubleCount } = useValues(logic);
return (
<p>
{count} {doubleCount}
</p>
);
};
export default function App() {
const { setCount } = useActions(logic);
const { count } = useValues(logic);
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>Increment</button>
<Count />
</div>
);
}
In the code above, we have our saga methods in the object that we pass into the kea
function. The takeEvery
is called every time a new value is emitted, so we can use it to run code like another action like we did above.
We use the yield
keyword to return the value that’s used to set the action. put
is used to schedule the dispatching of action from the store.
this.actions.setDoubleCount(count * 2)
returns the value that we want to emit for setDoubleCount
, so yield
and put
together will send the action to the setDoubleCount
and emit the value to our components via the useValue
hook.
The start
method is a generator function that’s called when our store initializes, so we can put any store initialization code inside.
Therefore, when we click the increment button, the setCount
function is called, which updates the count
state in the store. Then the takeEvery
method is called, which dispatches the setDoubleCount
action. Then that value is emitted and ends up in the Count
component.
So the left number will increment by 1 and the right one will increment by 2.
Thunks
Thunks are another way to commit side effects with Redux. It lets us dispatch multiple actions at once and also lets us run async code with Redux. It does the same things in Kea.
To use thunks with Kea, we install the Kea Thunk and Redux Thunk packages as follows:
//index.js
import React from "react";
import ReactDOM from "react-dom";
import { resetContext, getContext } from "kea";
import { Provider } from "react-redux";
import thunkPlugin from "kea-thunk";
import App from "./App";
resetContext({
createStore: true,
plugins: [thunkPlugin]
});
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={getContext().store}>
<App />
</Provider>,
rootElement
);
//App.js
import React from "react";
import { kea, useActions, useValues } from "kea";
const delay = ms => new Promise(resolve => window.setTimeout(resolve, ms));
const logic = kea({
actions: () => ({
setCount: count => ({ count }),
setDoubleCount: doubleCount => ({ doubleCount })
}),
thunks: ({ actions, dispatch, getState }) => ({
setCountAsync: async count => {
await delay(1000);
actions.setCount(count);
await delay(1000);
actions.setDoubleCount(count * 2);
}
}),
reducers: ({ actions }) => ({
count: [
0,
{
[actions.setCount]: (state, payload) => payload.count
}
],
doubleCount: [
0,
{
[actions.setDoubleCount]: (state, payload) => payload.doubleCount
}
]
})
});
const Count = () => {
const { count, doubleCount } = useValues(logic);
return (
<p>
{count} {doubleCount}
</p>
);
};
export default function App() {
const { setCountAsync } = useActions(logic);
const { count } = useValues(logic);
return (
<div className="App">
<button onClick={() => setCountAsync(count + 1)}>Increment</button>
<Count />
</div>
);
}
In the code above, we added the kea-thunk
plugin with:
plugins: [thunkPlugin]
Then in the thunks
property of the object that we pass into the kea
function, we defined our thunk, which has the async delay
function to pause the thunk for 1 second. Then we dispatch the setCount
action and dispatch the setDoubleAction
after call delay
to wait another second.
We can’t run async code with actions functions since they’re supposed to be pure synchronous functions.
Using thunks is a good way to run async code when dispatching actions.
In the end, we should get the increment button, which we can click to increment the count
one second after the button is clicked and increment doubleCount
after two seconds.
Conclusion
Kea is an alternative to Redux for state management. It has various plugins to do state management like sagas and thunks.
It works similarly to how Redux works and uses Redux as a base for its state management solution.
It works by creating a store with actions and reducers. They are the same as what they are in Redux. Also, we can add listeners to listen to action dispatch events. We can also add sagas and thunks via Kea’s plugins.
Full visibility into production React apps
Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
The post Simplify React state management with Kea appeared first on LogRocket Blog.
Top comments (0)