jotai-recoil-adapter
is a library designed to facilitate the transition from Recoil to Jotai in React applications. It provides a drop-in Recoil-compatible API built on top of Jotai's state management capabilities.
- jotai-recoil-adapter
- Quick Start
- Usage
RecoilRoot
atom
atomAsync
selector
selectorDefault
selector
(asynchronous) asasyncSelector
selectorFamily
selectorFamilyDefault
atomFamily
atomFamilyAsync
useRecoilState
useRecoilValue
useSetRecoilState
useRecoilCallback
useRecoilBridgeAcrossReactRootsUNSTABLE
(use with React portal)useRecoilBridgeAcrossReactRootsUNSTABLE
(use with react-three-fiber)
- Use-Case
- Warnings and Guidance
- Contributing
- Support and Issues
- License
npm i jotai-recoil-adapter
The adapter supports the following Recoil features:
import {
// standard Recoil APIs
RecoilRoot,
atom,
atomFamily,
selector,
selectorFamily,
useRecoilCallback,
useRecoilState,
useRecoilValue,
useSetRecoilState,
useResetRecoilState,
useRecoilBridgeAcrossReactRoots_UNSTABLE,
// special adapters for compatibility
atomAsync,
atomFamilyAsync,
asyncSelector,
asyncSelectorFamily,
selectorDefault,
// non-standard adaptations, see Readme
waitForAll,
} from 'jotai-recoil-adapter';
Please read the Use Case and Warnings and Guidance section before use in business applications.
Convenience adapter for Recoil's <RecoilRoot />
component.
This component does nothing. It merely returns a <React.Fragment />
that wraps child components.
import { RecoilRoot } from 'jotai-recoil-adapter';
const App = () => <RecoilRoot> /* ... */ </RecoilRoot>
Basic atom creation:
import { atom } from 'jotai-recoil-adapter';
const textState = atom({
key: 'textState',
default: 'Hello',
effects: "UNSUPPORTED"
});
Adapter for Recoil atom
that are initialized with an async selector. To be used with the selectorDefault
adapter as a default value.
interface AtomAsyncAdapterParams<T, U> {
...,
default: Promise<T>;
effects?: "UNSUPPORTED";
fallback?: U;
};
import { atom, atomAsync, selectorDefault } from 'jotai-recoil-adapter';
const userIdState = atom({
key: 'userId',
default: 1,
});
const userAtom = atomAsync({
key: 'userAtom',
default: selectorDefault({
key: 'userAtomDefaultValueSelector',
get: async ({ get }) => {
const userId = get(userIdState);
const response = await fetch(`/api/user/${userId}`);
return response.json();
},
}),
fallback: /* ... */
});
Why is the API different from Recoil? Jotai's API isn't perfectly compatible with Recoil's. Remember: this adapter exists to help ease the migration from Recoil to Jotai.
Important: This adapter is implemented using Jotai's getDefaultStore
method, therefore this is incompatible with custom Jotai store providers and must only be used in Jotai providerless mode.
Basic selector usage:
import { atom, selector } from 'jotai-recoil-adapter';
const countState = atom({
key: 'count-state',
default: Math.PI
});
const doubleCountSelector = selector({
key: 'double-count',
get: ({ get }) => get(countState) * 2,
});
jotai-recoil-adapter
allows seamless composition of Jotai atom
and atomFamily
with its selector
, selectorFamily
, and useRecoilCallback
. This enables complex state management patterns while maintaining a consistent API.
In this example, a Jotai PrimitiveAtom
is used with a selector
from jotai-recoil-adapter
. This pattern is extendable to other combinations, allowing you to take full advantage of Jotai's capabilities within a Recoil-like API.
import { atom as jotaiAtom } from 'jotai';
import { selector, useRecoilValue } from 'jotai-recoil-adapter';
const countState = jotaiAtom(0);
const doubleCountSelector = selector({
key: '...',
get: ({ get }) => get(countState) * 2,
});
Special adapter for both synchronous and asynchronous Recoil selector
that are used to initialize recoil atom
.
import {
atom,
atomFamily,
selectorDefault
} from 'jotai-recoil-adapter';
const idState = atom({
key: '...',
default: 1,
});
const itemAtom = atom({
key: '...',
default: selectorDefault({
key: '...',
get: ({ get }) => {
const id = get(idState);
return createItem(id);
},
}),
});
const itemStateFamily = atomFamily({
key: '...',
default: (itemId: string) => `Item ${itemId}`,
});
const itemStateFamily = atomFamily({
key: '...',
default: selectorDefault({
key: '...',
get: ({ get }) => {
...
}
}),
});
Important: This adapter is implemented using Jotai's getDefaultStore
method, therefore this is incompatible with custom Jotai store providers and must only be used in Jotai providerless mode.
Asynchronous selector implemented using a special adapter:
type AsyncRecoilSelectorOptions<T, U> = {
key: string;
get: ({ get }) => Promise<T>;
fallback: U;
};
import { asyncSelector } from 'jotai-recoil-adapter';
const randomNumberSelector = asyncSelector<number, "Crunching numbers...">({
key: 'randomNumberSelector',
get: async ({ get }) => {
await new Promise(resolve => setTimeout(resolve, 1000));
return Math.random();
},
fallback: "Crunching numbers..."
});
const userDataSelector = asyncSelector<User, null>({
key: 'userData',
get: async ({ get }) => {
const response = await fetch('/api/user/data');
return response.json();
},
fallback: null
});
Atom family for creating a series of atoms based on parameters:
import { selectorFamily } from 'jotai-recoil-adapter';
const itemStateFamily = selectorFamily({
key: '...',
default: (itemId: string) => ({ get }) => {
/* ... */
},
});
Special adapter for both synchronous and asynchronous Recoil selectorFamily
that are used to initialize recoil atomFamily
.
import {
atomFamily,
selectorDefaultFamily
} from 'jotai-recoil-adapter';
const itemStateFamily = atomFamily({
key: '...',
default: selectorDefaultFamily({
key: '...',
get: (itemId: string) => ({ get }) => {
...
}
}),
});
Important: This adapter is implemented using Jotai's getDefaultStore
method, therefore this is incompatible with custom Jotai store providers and must only be used in Jotai providerless mode.
Atom family for creating a series of atoms based on parameters:
import {
atomFamily,
selectorDefault,
selectorDefaultFamily
} from 'jotai-recoil-adapter';
const itemStateFamily = atomFamily({
key: `item-state-family`,
default: (itemId: string) => `Item ${itemId}`,
});
const itemStateFamily = atomFamily({
key: '...',
default: selectorDefault({
key: '...',
get: ({ get }) => {
...
}
}),
});
const itemStateFamily = atomFamily({
key: '...',
default: selectorDefaultFamily({
key: '...',
get: (itemId: string) => ({ get }) => {
...
}
}),
});
Adapter for Recoil atomFamily
that are initialized with an async selector. To be used with the selectorDefaultFamily
and selectorDefault
adapters as a default values:
interface AtomFamilyAsyncAdapterParams<T, Param, U> {
default: Promise<T>;
effects?: "UNSUPPORTED";
fallback?: U;
}
import { selectorDefault, selectorDefaultFamily, atomAsync } from 'jotai-recoil-adapter';
const fooStateFamily = atomFamilyAsync({
key: 'foo-atom-family',
default: selectorDefaultFamily({
key: 'foo-atom-family-default-value-selector',
get: (param) => async ({ get }) => {
const data = await fetchData(`v1/api/data/${ param }`);
const composedValue = get(someOtherAtom);
return doStuffWith(composedValue, data);
}
}),
fallback: /* ... */
});
const barStateFamily = atomFamilyAsync({
key: 'bar-atom-family',
default: selectorDefault({
key: 'bar-atom-family-default-value-selector',
get: async ({ get }) => {
const data = await fetchData();
const composedValue = get(someOtherAtom);
return doStuffWith(composedValue, data);
}
}),
fallback: /* ... */
});
Important: This adapter is implemented using Jotai's getDefaultStore
method, therefore this is incompatible with custom Jotai store providers and must only be used in Jotai providerless mode.
Hook to read and write atom state:
import { useRecoilState } from 'jotai-recoil-adapter';
const [text, setText] = useRecoilState(textState);
Hook to read atom or selector state:
import { useRecoilValue } from 'jotai-recoil-adapter';
const count = useRecoilValue(countSelector);
Hook to update atom state:
import { useSetRecoilState } from 'jotai-recoil-adapter';
const setCount = useSetRecoilState(countState);
Hook to interact with multiple atoms/selectors:
import { useRecoilCallback } from 'jotai-recoil-adapter';
const logCount = useRecoilCallback(
({ snapshot }) => async () => {
const count = await snapshot.getPromise(countState);
console.log(count);
},
[...]
);
Use Jotai's Provider
component.
Recommendation is to use Jotai's "providerless mode".
see: pmndrs/jotai#683 (comment)
The primary use-case for jotai-recoil-adapter
is to facilitate an incremental migration from Recoil to Jotai. This is particularly useful in scenarios where:
- Large Codebases: Applications with a significant investment in Recoil can migrate to Jotai gradually, without needing a complete rewrite.
- Testing and Stability: It allows for piece-by-piece migration and testing, ensuring that the application remains stable and reliable throughout the process.
- Learning Curve: Teams can adapt to Jotai's concepts and APIs at a comfortable pace, reducing the learning curve.
- Coexistence: Enables the coexistence of Recoil and Jotai during the transition period, allowing for a phased-out deprecation of Recoil.
This approach minimizes disruption in development workflows and provides a path to leverage Jotai's simplicity and performance benefits without the upfront cost of a full-scale migration.
While jotai-recoil-adapter
aims to provide a seamless transition from Recoil to Jotai, there are potential performance implications to consider:
- Overhead: The adapter introduces an additional abstraction layer, which could lead to slight performance overhead compared to using Jotai or Recoil directly.
- Optimization Differences: Jotai and Recoil have different optimization strategies. Be aware that performance characteristics may change when migrating from Recoil to Jotai.
You should thoroughly test and measure your application before and after applying this adapter to insure against regressions.
- Context Propagation: The context propagation mechanisms in Jotai and Recoil are different. This could lead to unexpected behavior, especially in complex component trees or when using context-dependent features.
- Error Handling: The error handling paradigms in Jotai and Recoil might differ. Pay attention to how errors are handled and propagated in your application after the migration.
jotai-recoil-adapter
does not cover the entire API surface of Recoil. Some advanced features and utilities provided by Recoil might not have equivalents in this adapter.
It's important to review your current usage of Recoil and determine if any advanced features critical to your application are not supported by the adapter. Plan for alternative solutions or adjustments in such cases.
Contributions to jotai-recoil-adapter
are welcome. To contribute, fork the repository, create a feature branch, commit your changes, and open a Pull Request.
See CONTRIBUTING.md
For issues or support questions, please file an issue on the GitHub repository.
jotai-recoil-adapter
is released under the MIT License.