How to use Recoil in React
Managing global state in React often becomes a bottleneck when prop-drilling or Context API limitations begin to impact performance and developer experience. With over 25 years of experience in software development and as the creator of CoreUI, I have architected dozens of enterprise-grade dashboard templates where state synchronization is critical. From my expertise, the most efficient and modern solution for fine-grained state management is Recoil, which introduces a graph-based approach using atoms and selectors. This library provides a highly performant way to share state across components without the boilerplate of Redux or the re-render issues sometimes found in complex Context providers.
Use the RecoilRoot component to wrap your application and the atom function to define shared pieces of state.
import React from 'react'
import { RecoilRoot, atom, useRecoilState } from 'recoil'
const countState = atom({
key: 'countState',
default: 0
})
const Counter = () => {
const [count, setCount] = useRecoilState(countState)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
const App = () => (
<RecoilRoot>
<Counter />
</RecoilRoot>
)
export default App
Recoil operates by creating a state tree that is separate from the React component tree. The RecoilRoot serves as the provider for this external state. Atoms are units of state that components can subscribe to; when an atom is updated, every component subscribed to that atom re-renders with the new value. The useRecoilState hook works exactly like React standard useState, but it targets the shared atom instead of local component memory.
1. Setting Up the RecoilRoot
The foundation of any Recoil-powered application is the RecoilRoot. This component must wrap any part of your component tree that needs access to Recoil state, typically at the very top of your application file.
import React from 'react'
import { RecoilRoot } from 'recoil'
import MainLayout from './MainLayout'
const App = () => {
return (
<RecoilRoot>
<MainLayout />
</RecoilRoot>
)
}
export default App
By placing RecoilRoot at the root, you ensure that any component within your application, no matter how deeply nested, can access global state without passing props through intermediate layers. This is the same principle we follow in our Admin Dashboard Template to keep layout components decoupled.
2. Defining Atoms for Shared State
An atom represents a piece of state. Atoms can be read from and written to by any component. When an atom is updated, each subscribed component is re-rendered with the new value.
import { atom } from 'recoil'
export const sidebarVisibleState = atom({
key: 'sidebarVisibleState',
default: true
})
export const userSettingsState = atom({
key: 'userSettingsState',
default: {
theme: 'light',
notifications: true
}
})
Every atom requires a unique key, which is used internally by Recoil to identify the state piece and for debugging purposes. The default value can be any JavaScript type, including objects and arrays.
3. Using useRecoilState for Interactive Components
The useRecoilState hook is the primary way to interact with an atom. It returns a pair: the current value of the state and a setter function to update it.
import React from 'react'
import { useRecoilState } from 'recoil'
import { sidebarVisibleState } from './atoms'
import { CButton } from '@coreui/react'
const SidebarToggle = () => {
const [isVisible, setIsVisible] = useRecoilState(sidebarVisibleState)
return (
<CButton
color='primary'
onClick={() => setIsVisible(!isVisible)}
>
{isVisible ? 'Hide Sidebar' : 'Show Sidebar'}
</CButton>
)
}
export default SidebarToggle
This hook makes transitioning from local state to global state seamless. In CoreUI components, we often recommend this pattern for UI elements like a Sidebar that need to be controlled from a distant header or settings panel.
4. Reading State with useRecoilValue
If a component only needs to read the state without modifying it, useRecoilValue is the more efficient choice. It prevents the component from being unnecessary re-rendered if you only needed the setter logic elsewhere.
import React from 'react'
import { useRecoilValue } from 'recoil'
import { sidebarVisibleState } from './atoms'
import { CSidebar } from '@coreui/react'
const AppSidebar = () => {
const isVisible = useRecoilValue(sidebarVisibleState)
return (
<CSidebar visible={isVisible}>
{/* Sidebar content here */}
</CSidebar>
)
}
export default AppSidebar
Using specialized hooks helps clarify the intent of your component. It is a best practice to use useRecoilValue when no updates are performed, keeping your components clean and focused.
5. Derived State with Selectors
Selectors allow you to derive state from atoms or other selectors. They are pure functions that calculate a value based on the current state. This is useful for filtering lists or calculating totals.
import { selector } from 'recoil'
import { todoListState } from './atoms'
export const todoListStatsState = selector({
key: 'todoListStatsState',
get: ({ get }) => {
const list = get(todoListState)
const totalNum = list.length
const totalCompletedNum = list.filter((item) => item.isComplete).length
const totalUncompletedNum = totalNum - totalCompletedNum
const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum
return {
totalNum,
totalCompletedNum,
totalUncompletedNum,
percentCompleted,
}
}
})
When you need to handle array calculations, you might find it useful to check how to get the length of an array in javascript to ensure your selector logic is robust. Selectors are cached, meaning they only re-run when their dependencies (the atoms they “get”) change.
6. Filtering Data in Selectors
A common use case for selectors is providing a filtered view of a larger dataset. This keeps the original data intact while providing specialized views for specific UI components.
import { selector } from 'recoil'
import { todoListState, todoListFilterState } from './atoms'
export const filteredTodoListState = selector({
key: 'filteredTodoListState',
get: ({ get }) => {
const filter = get(todoListFilterState)
const list = get(todoListState)
switch (filter) {
case 'Show Completed':
return list.filter((item) => item.isComplete)
case 'Show Uncompleted':
return list.filter((item) => !item.isComplete)
default:
return list
}
}
})
For more advanced filtering techniques, you can refer to our guide on how to filter an array in javascript. This pattern ensures that your components stay thin, as the filtering logic lives entirely within the Recoil selector graph.
7. Performance and Best Practices
One of the greatest strengths of Recoil is its ability to handle thousands of atoms without the performance degradation typically associated with the Context API. Since components only subscribe to specific atoms, an update to one part of the state won’t trigger a global re-render.
// Best practice: Write-only hook
import { useSetRecoilState } from 'recoil'
import { userSettingsState } from './atoms'
const ResetSettings = () => {
const setSettings = useSetRecoilState(userSettingsState)
const handleReset = () => {
setSettings({ theme: 'light', notifications: true })
}
return <button onClick={handleReset}>Reset to Defaults</button>
}
By using useSetRecoilState, the ResetSettings component itself doesn’t even re-render when the settings change, because it doesn’t “read” the value. This “write-only” subscription is a powerful optimization for high-performance applications.
Best Practice Note:
Always keep your atom keys unique across the entire project to avoid collisions. This is the same rigorous approach we use in CoreUI to ensure that component states don’t conflict in complex environments.
For large applications, organize your atoms and selectors into a dedicated state/ or recoil/ folder to maintain scalability as your project grows.



