Lawrence Eagles Senior full-stack developer, writer, and instructor.

Getting started with radioactive state in React

8 min read 2338

Getting started with radioactive state in React

Radioactive state is a deeply reactive state. When mutated (shallow or deep), it automatically triggers a render update. This eliminates the need for setting state, the hassles of creating a new state, and the awkwardness of dealing with stale state.

It is quite the endeavor to manage complex state with the useState Hook. This is considered an anti-pattern in the React community.

Also, the useState Hook offers no provision for instantaneous access to fresh state, after a new state is set. And because the state is only updated after a render update we still have to deal with stale state.

With radioactive state, we eliminate these challenges, boost performance, and enjoy new features.

Getting started

To get started using radioactive state, install the package by running these commands:

npm i radioactive-state or yarn add radioactive-state

This provides the useRS Hook which enables us to create a radioactive state in our components. We initialize the state using the useRS Hook, which takes an object as its argument. Each property of this object refers to a different state and it can be mutated without having to set state:

const state = useRS({ count: 0 }); // initializes the state

To mutate this state we use the syntax below:

const increment = () => state.count++; // increases the count
const decrement = () => state.count--; // decreates the count

The counter component

import "./styles.css";
import React from "react";
import useRS from "radioactive-state";
export default function App() {
  const state = useRS({ count: 0 });
  const increment = () => state.count++;
  const decrement = () => state.count--;
  return (
    <div className="app container d-flex flex-column justify-content-center align-items-center">
      <div className="mb-4">
        <span className="count">{state.count}</span>
      </div>
      <article className="d-flex">
        <button className="mx-2 btn btn-success btn-sm" onClick={increment}>
          increment count
        </button>
        <button className="mx-2 btn btn-danger btn-sm" onClick={decrement}>
          increment count
        </button>
      </article>
    </div>
  );
}

Above, is a simple counter component that increments or decrements the value of count when their buttons are clicked. You can play with the code here.

Following this succinct elaboration, we have barely scratched the surface of radioactive state.

Features

Always fresh state, unlike useState

When managing state using the useState Hook. Our component only gets a fresh state after a render update. This can cause some nasty bugs that are hard to debug. Let’s look at some examples:

import React, { useState } from "react";
import "./styles.css";
const App = () => {
  const [count, setCount] = useState(0);
  const increment = () => {
    console.log("before: ", count);
    setCount(count + 1);
    console.log("after: ", count);
  };
  return (
    <div className="App">
      <div className="app
           container
           d-flex
           flex-column
           justify-content-center
           align-items-center"
      >
        <div className="mb-5">
          <span className="count">{count}</span>
        </div>
        <article className="d-flex">
          <button
            className="mx-2 p-3 btn btn-success btn-sm"
            onClick={increment}
          >
            increment count
          </button>
        </article>
      </div>
    </div>
  );
};
export default App;

Above is a simple component with a count state and an increment function which calls the useState setter function under the hood to update the count state. The current value of count is displayed in the UI.

We notice that when we increment the count it reflects on the UI but the count logged to the console is still 0.

increment counter in code sandbox

The count value logged to the console is always less than the value in the UI by one. You can play with code here.

With radioactive state, we don’t have this issue:

import "./styles.css";
import React from "react";
import useRS from "radioactive-state";
const App = () => {
  const state = useRS({ count: 0 });
  const increment = () => {
    console.log("before: ", state.count);
    state.count++;
    console.log("after: ", state.count);
  };
  return (
    <div className="app
          container
          d-flex
          flex-column
          justify-content-center
          align-items-center"
    >
      <div className="mb-5">
        <span className="count">{state.count}</span>
      </div>
      <article className="d-flex">
        <button className="mx-2 p-3 btn btn-success btn-sm" onClick={increment}>
          increment count
        </button>
      </article>
    </div>
  );
};
export default App;

Above is the implementation of the same app but using useRS Hook. The issue is not experienced thanks to radioactive state.

useRS increment counter

You can play with code here. In the image above, we can see from the console that the value of the count state before and after it has been incremented is 0 and 1 respectively. This is because of the reactivity of radioactive state.

Deeply reactive, directly mutate state at any level to update components

Here we will look at how radioactive state solves the stale state problem in React:

import "./styles.css";
import React, { useState } from "react";
export default function Example() {
  const [count, setCount] = useState(0);
  const lazyIncrement = () => {
    setTimeout(() => {
      setCount(count + 1);
    }, 3000);
  };
  return (
    <div className="app
          container
          d-flex
          flex-column
          justify-content-center
          align-items-center"
    >
      <div className="mb-5">
        <span className="count">{count}</span>
      </div>
      <article className="d-flex flex-column">
        <button
          className="mx-2 p-3 btn btn-success btn-sm"
          onClick={lazyIncrement}
        >
          increment count
        </button>
        <small className="m-2">
          <strong>Increment the count a number of times!</strong>
        </small>
      </article>
    </div>
  );
}

You can play with the code here. Above is a React component that manages the count state using the useState Hook. When the show count button is clicked the lazyIncrement function is called to update the state. But the useState‘s setter function (setCount) gets called after 3000 milliseconds because of the setTimeout function. Consequently, the state gets updated only after 3000 milliseconds.

When we click the button n times to increment the state, we see that the state is only implemented once. This happens because setCount continuously gets called with the old state. It only gets a fresh state when the component rerenders.

To mitigate this issue we often pass an updater function to the useState setter function. This would take the previous state (prevState) as the parameter and use it to compute the value of the nextState. Thus the problem above could be solved with the code below:

setCount(prevCount => prevCount++)

However, this gets awkward when you want to update other states based on the new state value.

A much cleaner approach to solve this stale state problem is to use radioactive state. Since it gives us a truly reactive state, we can reimplement our component like this:

import "./styles.css";
import React from "react";
import useRS from "radioactive-state";
export default function Example() {
  const count = useRS({ value: 0 });
  const lazyIncrement = () => {
    setTimeout(() => {
      count.value++;
    }, 3000);
  };
  return (
    <div className="app
          container
          d-flex
          flex-column
          justify-content-center
          align-items-center"
    >
      <div className="mb-5">
        <span className="count">{count.value}</span>
      </div>
      <article className="d-flex flex-column">
        <button
          className="mx-2 p-3 btn btn-success btn-sm"
          onClick={lazyIncrement}
        >
          increment count
        </button>
        <small className="m-2">
          <strong>Increment the count a number of times!</strong>
        </small>
      </article>
    </div>
  );
}

Because we now have a truly reactive fresh state, it is able to compute the correct value of the new state. Even though the state is updated after 3000 milliseconds. You can play with code here.

Reactive bindings for inputs

React is great but when it comes to form handling many developers prefer using a third-party library. React forms are usually a composition of controlled components. Working with these involves a lot of repetitive and annoying stuff such as keeping track of values and errors.

We can create a controlled component like this:

const [email, setEmail] = useState("");
  return (
    <div className="App">
      <form>
        <label>Email:</label>
        <input
          value={email}
          placeholder="Enter Email"
          onChange={(e) => setEmail(e.target.value)}
          type="text"
        />
      </form>
    </div>

Notice we have to keep track of the value using e.target.value. If we were working with checkboxes this would be e.target.checked. It becomes even harder if our form has different inputs e.g checkbox, range, radio, etc.

Radioactive state provides a binding API that binds an input’s value to a key in state. This feature uses an es6 spread operator like this:

<input {...state.$key}  />

We simply prefix the key with $ and access it as shown above.



The binding API determines the type of input by using the initial value of each state as property state. Consequently state.$key would return the following:

  • An object containing value and onChange, if the initial value is a type it’s a string or number

If the initial value type is number, the onChange function would convert the e.target.value from string to number then save it in the key

  • An object containing checked and onChange props. And uses e.target.checked internally in the onChange function, if the initial value type is boolean, state.$key

The implementation would look like this:

const state = useRS({
  name: "iPhone 8",
  desc: "The mobile phone",
  promoPrice: 500,
  price: 1000,
  sold: true,
  color: "red"
});
const { $promoPrice, $price, $name, $sold, $color } = state;

And the bindings would be used like this:

<input className="form-control" {...$name} type="text" />

I have built a product filter component with reactive state using these bindings. You can play with the code here.

No extra rerenders – auto mutation batching

It is important to note here that when using reactive state we must pass an object to the Hook.

Correct

const state = useRS({count: 0})

Wrong

const state = useRS(0)

Also, consider the function below:

const customFunc = () => {
  state.name = "Lawrence";
  state.teams.frontend.react.push(["Lawrence", "Dave"]);
  state.commits++;
  state.teams.splice(10, 1);
  state.tasks = state.tasks.filter(x => x.completed);
};

When the above function gets called, the question arises if it would trigger multiple render updates. But it wouldn’t. And this is because in reactive state, mutations are batched into a single mutation. Consequently, no matter how many times the state is mutated it would only trigger on rerender.

You can get more details on this here.

Reactive props

This is probably the most interesting feature of radioactive state. React has a unilateral flow of data. This means that its data flows downwards, from parent to child components. This data (props )are usually immutable and changing them has no effect on the parent component.

A parent component can pass its state as props to a child component as seen below:

export default function App() {
  const [bulbSwitch, setBulbSwitch] = useState(false);

  return (
    <div className="App">
      <Bulb bulbSwitch={bulbSwitch} />
    </div>
  );
}

Mutating the bulbSwitch state in the child component would not trigger a render update in the parent component.

However, reactive state changes things. When used, a child component can trigger a rerender in a parent component by mutating the state. And this can be a very powerful feature that also eliminates the need to memoize that component. Learn more on this here.

Radioactive state is blazing fast

Of course, our discourse is not complete if we do not talk about the performance implication of using reactive state.
Well, reactive state is fast. Blazing fast. 25% faster than the useState Hook and this is for a fairly complex app. It continuously outperforms the useState Hook as the state gets more complex.

This number is derived from an average of 100 performance tests where an array of 200 objects is rendered and various operations like adding, removing, reordering, and mutations were done one after another — Reactivestate doc

When we use the useState Hook, a new state is created every-time we want to update state. And the setter function is called with this new state to update the state. Radioactive state does not create a new state every time a state is updated. This is one major reason it outperforms useState.


More great articles from LogRocket:


Also, radioactive state creates a deeply reactive state by recursively proxifying the state using JavaScript proxy. Get more on this here.

Mutation pitfalls to avoid

While radioactive state is amazing there are some pitfalls to avoid while using it. We will consider the patterns to avoid these in this section.

Dealing with expensive initial state

If the initial state is gotten from an expensive or a long-running computation, it would be inefficient to initialize the state as seen below:

const state = useRS({
  x: getData(); // if getData is an expensive operation.
})

The above pattern would trigger the getData function every time the component renders. This is not what we want. This is an anti-pattern. The correct approach is shown below:

const state = useRS({
  x: getData;
})

Now this would only call the getData function once to initialize the state, consequently, it is much more efficient.

Mutation flag

Consider the code below:

const state = useRS({
  users: []
})

useEffect( () => {
  // do something ...
}, [state.users])


const addUser = (user) => state.users.push(user)

Above is a small contrived example to illustrate this issue. When the addUser function is called a new user is added to the users array in state.

However, the useEffect Hook would not run. Take note the user state is mutated by calling the addUser function.

This is because when we mutate a reference type data (such as an array or an object), in state, it’s reference stays the same. Notice that this reference type date was passed as dependency to the useEffect hook. And since it did not change (even when we called addUser), the useEffect Hook did not run.

This is no doubt a weird bug. To fix this we pass state.users.$ as a dependency to useEffect instead of state.users as seen below:

const state = useRS( { users: [] })

useEffect( () => {
  // do something.
}, [state.users.$])

Learn more about this here.

Final thoughts

Reactive state brings a revolutionary innovation, to the React world. It is blazing fast, highly reactive, and efficient. It provides cleaner patterns to avoid common pitfalls in React state management. And it shines brighter as the state becomes more complex. After this post, I believe you should be able to use reactive state without any hassle, but just keep the “gotchas” in mind.

LogRocket: Full visibility into your production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard 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 combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?

Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.

No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.

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 — .

Lawrence Eagles Senior full-stack developer, writer, and instructor.

Leave a Reply