DEV Community

Cover image for Simplify React state management with Kea
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Simplify React state management with Kea

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.

LogRocket Free Trial Banner

Basic state management

We can get started by creating an app with create -react-app by running:

npx create-react-app kea-app
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode
//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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
      }
    ]
  })
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode
//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>
  );
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode
//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>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we added the kea-thunk plugin with:

plugins: [thunkPlugin]
Enter fullscreen mode Exit fullscreen mode

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.

Alt Text

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)