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.
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
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.
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
.
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
.
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.
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.
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:
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
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.
It is important to note here that when using reactive state
we must pass an object to the Hook.
const state = useRS({count: 0})
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.
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.
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
.
Also, radioactive state creates a deeply reactive state by recursively proxifying the state using JavaScript proxy
. Get more on this here.
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.
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.
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.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]