Understanding Selectors in Recoil
What Are Selectors?
A selector is a pure function in Recoil that derives or computes state based on atoms or other selectors. It’s similar to the concept of "computed properties" in other frameworks (like Vue.js), where the value of the selector changes when its dependencies change.
Selectors can either get or set state.
They can be thought of as a "view" on top of state: a transformation of atoms and/or other selectors.
Basic Selector Example
Let’s break down how a selector works. Imagine you have an atom that holds a piece of state (like a string of text), and you want to compute the length of that string. The selector derives this length from the atom.
1. Define an Atom:
An atom is a piece of state in Recoil that components can subscribe to.
import { atom } from 'recoil';
export const textState = atom({
key: 'textState', // Unique ID for this atom
default: '', // Default value (initial state)
});
2. Define a Selector:
A selector derives or computes new state based on the atom.
import { selector } from 'recoil';
import { textState } from './atoms';
export const charCountState = selector({
key: 'charCountState', // Unique ID for this selector
get: ({ get }) => { // `get` allows access to atoms/selectors
const text = get(textState); // Get the value of `textState`
return text.length; // Return the length of the text
},
});
Key: A unique identifier for the selector.
Get: A function that calculates the derived value. The
getfunction has access to the state usingget().
3. Using a Selector in a Component:
You can use the useRecoilValue hook to retrieve the computed value from the selector.
import { useRecoilValue } from 'recoil';
import { charCountState } from './selectors';
function CharacterCount() {
const count = useRecoilValue(charCountState); // Get the derived value
return <div>Character Count: {count}</div>;
}
- When the
textStateatom changes, thecharCountStateselector will recompute, and any component subscribed to the selector will re-render.
Selector Caching
Recoil automatically caches the value of selectors. When a selector is re-evaluated, Recoil will use the cached value if the underlying state (atoms/selectors) has not changed. This caching mechanism ensures efficient re-renders in large applications.
Asynchronous Selectors
Selectors in Recoil can also be asynchronous. They can fetch data from APIs, perform async operations, or return promises that resolve later.
Example: Fetching Data with an Async Selector
Here’s how you can define a selector that fetches data from an API:
import { selector } from 'recoil';
export const userDataState = selector({
key: 'userDataState',
get: async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
const data = await response.json();
return data; // Return the fetched data
},
});
- Async Selector: The
getfunction can return aPromise. Recoil will automatically handle the state while the promise is resolving (such as showing loading states in components).
Using the Async Selector in a Component:
import { useRecoilValue } from 'recoil';
import { userDataState } from './selectors';
function UserInfo() {
const user = useRecoilValue(userDataState); // Read the fetched data
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
Recoil automatically handles loading and errors when dealing with async selectors. If the async operation is ongoing, the component will suspend rendering, which is especially useful if you are using React Suspense.
Writing to Selectors
In addition to reading (getting) derived state, Recoil also supports writing to selectors by providing a set function. This allows selectors to act as both readers and writers of state.
Example: A Read/Write Selector
Suppose you want to create a selector that reads from an atom but also allows you to set a derived value. Here’s an example that manages a text state in lowercase:
import { selector } from 'recoil';
import { textState } from './atoms';
export const lowercaseTextState = selector({
key: 'lowercaseTextState',
get: ({ get }) => {
const text = get(textState); // Get the current value of `textState`
return text.toLowerCase(); // Return it in lowercase
},
set: ({ set }, newValue) => {
set(textState, newValue.toUpperCase()); // Set the original state in uppercase
},
});
Get Function: Returns the lowercase version of
textState.Set Function: When you try to update
lowercaseTextState, it updatestextStatewith the uppercase value.
Using the Read/Write Selector:
import { useRecoilState } from 'recoil';
import { lowercaseTextState } from './selectors';
function TextTransformer() {
const [text, setText] = useRecoilState(lowercaseTextState); // Access and modify the derived state
const onChange = (event) => {
setText(event.target.value); // Updates the state with uppercase
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
<p>Lowercase: {text}</p>
</div>
);
}
When the user types, the input value is stored in uppercase, but what the user sees is the lowercase transformation.
Dependent Selectors
Selectors can depend on other selectors. This allows you to build complex state dependencies with ease. For example, you could have one selector that processes raw data, and another selector that refines or filters that processed data.
export const filteredListState = selector({
key: 'filteredListState',
get: ({ get }) => {
const list = get(someListState);
const filter = get(filterState);
return list.filter(item => item.includes(filter));
},
});
- In this case,
filteredListStatedepends on bothsomeListStateandfilterState. It will automatically update if either of those atoms change.
When to Use Selectors?
Derived State: If you need to compute or derive values from the state (e.g., a transformation like filtering or aggregating data).
Shared Logic: When multiple components need access to the same computed state.
Asynchronous Data: For fetching data from APIs or performing async computations.
Composing State: When your state depends on multiple atoms or selectors, making selectors ideal for managing more complex or interdependent state.
Selector Best Practices
Keep Selectors Pure: Always ensure the
getfunction is pure—no side effects, only transformations of atoms/selectors.Efficiency: Since selectors are cached, try to avoid complex, unnecessary computations inside them.
Composability: Use selectors to compose state, so individual components don’t have to handle the complexity of computing derived state themselves.
Selectors are a powerful tool in Recoil for managing derived, computed, and async state, and they provide an efficient, declarative way to manage complex state dependencies in your React applications.




