Installations
npm install @reatom/npm-react
Developer Guide
Typescript
Yes
Module System
CommonJS, ESM
Node Version
18.20.5
NPM Version
10.8.2
Score
65.7
Supply Chain
99.4
Quality
89.5
Maintenance
100
Vulnerability
100
License
Releases
devtools: v0.8.0
Published on 17 Dec 2024
jsx: v3.16.0
Published on 17 Dec 2024
npm-react: v3.10.3
Published on 11 Dec 2024
lens: v3.11.6
Published on 11 Dec 2024
effects: v3.10.2
Published on 11 Dec 2024
core: v3.9.1
Published on 11 Dec 2024
Contributors
Languages
TypeScript (99.43%)
HTML (0.43%)
JavaScript (0.09%)
Shell (0.04%)
Developer
Download Statistics
Total Downloads
76,780
Last Day
43
Last Week
2,147
Last Month
7,891
Last Year
53,264
GitHub Statistics
1,108 Stars
2,356 Commits
113 Forks
21 Watching
37 Branches
75 Contributors
Package Meta Information
Latest Version
3.10.3
Package Id
@reatom/npm-react@3.10.3
Unpacked Size
67.59 kB
Size
18.65 kB
File Count
8
NPM Version
10.8.2
Node Version
18.20.5
Publised On
11 Dec 2024
Total Downloads
Cumulative downloads
Total Downloads
76,780
Last day
-36.8%
43
Compared to previous day
Last week
4.6%
2,147
Compared to previous week
Last month
-2%
7,891
Compared to previous month
Last year
138.1%
53,264
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
Peer Dependencies
1
Dev Dependencies
2
Adapter for react.
Installation
1npm i @reatom/npm-react
Also, you need to be installed @reatom/core
or @reatom/framework
and react
.
Read the handbook first for production usage.
Setup context
You need to set up the main context once and wrap your application in a provider at the top level.
1import { createCtx, connectLogger } from '@reatom/framework' 2import { reatomContext } from '@reatom/npm-react' 3import { Main } from './path/to/an/Main' 4 5const ctx = createCtx() 6if (import.meta.env.DEV) { 7 connectLogger(ctx) 8} 9 10export const App = () => ( 11 <reatomContext.Provider value={ctx}> 12 <Main /> 13 </reatomContext.Provider> 14)
Use atom
reatomComponent
The main API to bind atoms and actions to a component lifetime is reatomComponent
. It wraps your regular react component and put ctx
into the props. There is no additional rules or behavior, you can use any other hooks, accept props, return any valid ReactNode
. But if you using ctx.spy
, just like in any computed atom, it will subscribe to the passed atom and rerender from by changes.
1import { atom } from '@reatom/core' 2import { reatomComponent } from '@reatom/npm-react' 3 4export const countAtom = atom(0) 5export const Counter = reatomComponent( 6 ({ ctx }) => <input type="number" value={ctx.spy(count)} onChange={(e) => countAtom(ctx, e.target.valueAsNumber)} />, 7 'Counter', 8)
You can describe props types in the generic, it can be any kind of values, regular string, JSON, and atoms too. For example, here is a controlled component with atom state. Also, you can use additional bind
method instead of useAction
to bind an action to the component.
1import { atom, Atom } from '@reatom/core' 2import { reatomComponent } from '@reatom/npm-react' 3 4export const Counter = reatomComponent<{ 5 atom: Atom<number> 6 onChange: Action 7}>(({ ctx, atom, onChange }) => <input type="number" value={ctx.spy(atom)} onChange={ctx.bind(onChange)} />, 'Counter')
One of the most powerful features of reatomComponent
is that you are not bound by react hooks rules, you could use ctx.spy
in any order, right in your template.
1export const SomeList = reatomComponent( 2 ({ ctx }) => 3 ctx.spy(isLoadingAtom) ? ( 4 <span>Loading...</span> 5 ) : ( 6 <ul> 7 {ctx.spy(listAtom).map((el) => ( 8 <li>{el.text}</li> 9 ))} 10 </ul> 11 ), 12 'SomeList', 13)
Do not forget to put the component name to the second argument, it will increase your feature debug experience a lot!
Unmount
An important feature of reatomComponent
is automatic resource management with default for Reatom AbortController in the cause context. You may be familiar with this concept from @reatom/effects. The ctx
in reatomComponent props includes the AbortController which is followed by all derived actions. For example, it means if you will update an atom from the component and it will cause reatomResource refetch and the component will unmaunt before the fetch end - the fetch will throw an abort error.
This increases the stability of your application as it reduces the amount of possible race conditions. But be aware that sometimes you may want to create a request that you don't want to abort even if the unmount occurs. For example, it might be an analytic event, in which case you should use spawn.
useAtom
useAtom
is your main hook, when you need to describe reusable logic in hight order hook. It accepts an atom to read it value and subscribes to the changes, or a primitive value to create a new mutable atom and subscribe to it. It alike useState
, but with many additional features. It returns a tuple of [state, setState, theAtom, ctx]
. theAtom
is a reference to the passed or created atom.
In a component:
1import { action, atom } from '@reatom/core' 2import { useAction, useAtom } from '@reatom/npm-react' 3 4// base mutable atom 5const inputAtom = atom('', 'inputAtom') 6// computed readonly atom 7const greetingAtom = atom((ctx) => `Hello, ${ctx.spy(inputAtom)}!`, 'greetingAtom') 8// action to do things 9const onChange = action( 10 (ctx, event: React.ChangeEvent<HTMLInputElement>) => inputAtom(ctx, event.currentTarget.value), 11 'onChange', 12) 13 14export const Greeting = () => { 15 const [input] = useAtom(inputAtom) 16 const [greeting] = useAtom(greetingAtom) 17 const handleChange = useAction(onChange) 18 19 return ( 20 <> 21 <input value={input} onChange={handleChange} /> 22 {greeting} 23 </> 24 ) 25}
We recommend to setup logger here.
Use atom selector
Another use case for the hook is describing additional computations inside a component (create temporal computed atom). It is possible to put a reducer function to useState
, which will create a new computed atom (setState
will be undefined
in this case).
1import { useAtom } from '@reatom/npm-react' 2import { goodsAtom } from '~/goods/model' 3 4export const GoodsItem = ({ idx }: { idx: number }) => { 5 const [element] = useAtom((ctx) => ctx.spy(goodsAtom)[idx], [idx]) 6 7 return <some-jsx {...element} /> 8}
The reducer function is just the same as in atom
function. You could spy
a few other atoms. It will be called only when the dependencies change, so you could use conditions and Reatom will optimize your dependencies and subscribes only to the necessary atoms.
1import { useAtom } from '@reatom/npm-react' 2import { activeAtom, goodsAtom } from '~/goods/model' 3 4export const GoodsItem = ({ idx }: { idx: number }) => { 5 const [element] = useAtom((ctx) => (ctx.spy(activeAtom) === idx ? ctx.spy(listAtom)[idx] : null), [idx]) 6 7 if (!element) return null 8 9 return <some-jsx {...element} /> 10}
Advanced usage
Check this out!
1export const Greeting = ({ initialGreeting = '' }) => { 2 const [input, setInput, inputAtom] = useAtom(initialGreeting) 3 const [greeting] = useAtom((ctx) => `Hello, ${ctx.spy(inputAtom)}!`, [inputAtom]) 4 // you could do this 5 const handleChange = useCallback((event) => setInput(event.currentTarget.value), [setInput]) 6 // OR this 7 const handleChange = useAction((ctx, event) => inputAtom(ctx, event.currentTarget.value), [inputAtom]) 8 9 return ( 10 <> 11 <input value={input} onChange={handleChange} /> 12 {greeting} 13 </> 14 ) 15}
What, why? In the example bellow we creating "inline" atoms, which will live only during the component lifetime. Here are the benefits of this pattern instead of using regular hooks:
- You could depend your atoms by a props (deps changing will cause the callback rerun, the atom will the same).
- Easy access to services, in case you use reatom as a DI.
- Component inline atoms could be used for other computations, which could prevent rerenders (see above).
- Created actions and atoms will be visible in logger / debugger with async
cause
tracking, which is much better for debugging thanuseEffect
. - Unify codestyle for any state (local and global) description.
- Easy to refactor to global state.
Lazy reading
As react docs says, sometimes you need a callback, which depends on often changed value, but you don't want to change a reference of this handler, to not broke memoization of children components which depends on the current. In this case, you could use atom and read it value lazily.
Here is a standard react code, handleSubmit
reference is recreating on each input
change and rerender.
1const [input, setInput] = useState('') 2const handleSubmit = useCallback(() => props.onSubmit(input), [props.onSubmit, input])
Here handleSubmit
reference is stable and doesn't depend on input
, but have access to it last value.
1const [input, setInput, inputAtom, ctx] = useAtom('') 2const handleSubmit = useCallback(() => props.onSubmit(ctx.get(inputAtom)), [props.onSubmit, inputAtom, ctx])
Btw, you could use useAction
.
1const [input, setInput, inputAtom] = useAtom('') 2const handleSubmit = useAction((ctx) => props.onChange(ctx.get(inputAtom)), [props.onChange, inputAtom])
Prevent rerenders
useAtom
accepts third argument shouldSubscribe
which is true
by default. But sometimes you have a set of computations not all of which you need in the render. In this case you could use atoms from useAtom
without subscribing to it values.
Here is how could you share data created and managed in parent, but used in children.
1const [filter, setFilter, filterAtom] = useAtom('', [], false) 2const [data, setData, dataAtom] = useAtom([], [], false) 3const handleSubmit = useAction( 4 (ctx) => 5 ctx.schedule(() => 6 fetch(`api/search?q=${ctx.get(filterAtom)}`) 7 .then((res) => res.json()) 8 .then(setData), 9 ), 10 [filterAtom, dataAtom], 11) 12 13return ( 14 <> 15 <Filter atom={filterAtom} /> 16 <Table atom={dataAtom} /> 17 {/* this will not rerender by filters or data changes */} 18 <OtherComponent /> 19 </> 20)
Here is another example of in-render computations which could be archived without rerender.
1// this component will not rerender by `inputAtom` change, only by `numbers` change 2const [, , inputAtom] = useAtom('', [], false) 3const handleChange = useAction((ctx, event) => inputAtom(ctx, event.currentTarget.value), [inputAtom]) 4const [numbers] = useAtom((ctx) => ctx.spy(inputAtom).replace(/\D/g, ''), [inputAtom]) 5 6return ( 7 <> 8 <input onChange={handleChange} /> 9 numbers: {numbers} 10 </> 11) 12 13// onChange "q" - no rerender 14// onChange "qw" - no rerender 15// onChange "qw1" - rerender 16// onChange "qw1e" - no rerender
Use action
To bind your actions to relative context you need to use useAction
, it will just remove the first ctx
parameter from your action and return a function which accepts all other needed parameters.
1const pageAtom = atom(0, 'pageAtom') 2const next = action((ctx) => pageAtom(ctx, (page) => page + 1), 'pageAtom.next') 3const prev = action((ctx) => pageAtom(ctx, (page) => Math.max(1, page - 1)), 'pageAtom.prev') 4 5export const Paging = () => { 6 const [page] = useAtom(pageAtom) 7 const handleNext = useAction(next) 8 const handlePrev = useAction(prev) 9 10 return ( 11 <> 12 <button onClick={handlePrev}>prev</button> 13 {page} 14 <button onClick={handleNext}>next</button> 15 </> 16 ) 17}
useAction
accepts any function with ctx
parameter, not only action
, so you can write inline function, use props, and it will still memoized and return the same stable function reference, just like useEvent
1export const Paging = ({ pageAtom }: { pageAtom: Atom<number> }) => { 2 const [page] = useAtom(pageAtom) 3 const handleNext = useAction((ctx) => pageAtom(ctx, (page) => page + 1)) 4 const handlePrev = useAction((ctx) => pageAtom(ctx, (page) => Math.max(1, page - 1))) 5 6 return ( 7 <> 8 <button onClick={handlePrev}>prev</button> 9 {page} 10 <button onClick={handleNext}>next</button> 11 </> 12 ) 13}
Also, you can use useAction
to get an atom setter without subscribing to it.
1export const PagingAction = ({ pageAtom }: { pageAtom: Atom<number> }) => { 2 const setPage = useAction(pageAtom) 3 4 return ( 5 <> 6 <button onClick={() => setPage((page) => Math.max(1, page - 1))}>prev</button> 7 <button onClick={() => setPage((page) => page + 1)}>next</button> 8 </> 9 ) 10}
Use update
useUpdate
is a similar to useEffect
hook, but it allows you to subscribe to atoms and receive it values in the callback. Important semantic difference is that subscription to atoms works as onChange
hook and your callback will call during transaction, so you need to schedule an effects, but could mutate an atoms without batching. Subscriptions to a values works like regular useEffect
hook.
The most common use case for this hook is to synchronize some state from a props or context to an atom.
1import { action, atom } from '@reatom/core' 2import { useAction, useUpdate } from '@reatom/npm-react' 3import Form from 'form-library' 4 5const formValuesAtom = atom({}) 6const submit = action((ctx) => api.submit(ctx.get(formValuesAtom))) 7 8const Sync = () => { 9 const { values } = useFormState() 10 useUpdate((ctx, values) => formValuesAtom(ctx, values), [values]) 11 return null 12} 13// or just 14const Sync = () => useUpdate(formValuesAtom, [useFormState().values]) 15 16export const MyForm = () => { 17 const handleSubmit = useAction(submit) 18 19 return ( 20 <Form onSubmit={handleSubmit}> 21 <Sync /> 22 ..... 23 </Form> 24 ) 25}
And it works well in the opposite direction, you could synchronise an atom's data with the local state, or do any other kind of effect. You can use useUpdate
as a safety replacement for onChange
+ useEffect
.
For example, you need a controlled input from the passed atom.
Here is a naive implementation:
1export const Item = ({ itemAtom }) => { 2 const [value, setValue] = React.useState('') 3 4 React.useEffect(() => { 5 const cleanup = itemAtom.onChange((ctx, state) => setValue(state)) 6 // DO NOT FORGET TO RETURN THE CLEANUP 7 return cleanup 8 }, [itemAtom]) 9 10 return <input value={value} onChange={(e) => setValue(e.currentTarget.value)} /> 11}
Here is a simpler and more reliable implementation:
1export const Item = ({ itemAtom }) => { 2 const [value, setValue] = React.useState(itemAtom) 3 4 useUpdate((ctx, state) => setValue(state), [itemAtom]) 5 6 return <input value={value} onChange={(e) => setValue(e.currentTarget.value)} /> 7}
Use atom promise
If you have an atom with a promise and want to use its value directly, you could use useAtomPromise
. This function relies on React Suspense and throws the promise until it resolves. It can be useful with reatomResource.
1import { atom, reatomResource } from '@reatom/framework' 2import { useAtom, useAction, useAtomPromise } from '@reatom/npm-react' 3 4const pageAtom = atom(1, 'pageAtom') 5const listReaction = reatomResource(async (ctx) => { 6 const page = ctx.spy(pageAtom) 7 const response = await ctx.schedule(() => fetch(`/api/list?page=${page}`)) 8 if (!response.ok) throw new Error(response.statusText) 9 return response.json() 10}) 11 12export const List = () => { 13 const [page] = useAtom(pageAtom) 14 const prev = useAction((ctx) => pageAtom(ctx, (state) => Math.max(1, state - 1))) 15 const next = useAction((ctx) => pageAtom(ctx, (state) => state + 1)) 16 const list = useAtomPromise(listReaction.promiseAtom) 17 18 return ( 19 <section> 20 <ul> 21 {list.map((el) => ( 22 <li key={el.id}>...</li> 23 ))} 24 </ul> 25 <hr /> 26 <button onClick={prev}>prev</button> 27 {page} 28 <button onClick={next}>next</button> 29 </section> 30 ) 31}
Use context creator
Sometimes, you can only create ctx
inside a React component, for example, in SSR. For that case, we have the useCreateCtx
hook.
1export const App = () => { 2 const ctx = useCreateCtx((ctx) => { 3 // do not use logger in a server (SSR) 4 if (typeof window !== 'undefined') { 5 connectLogger(ctx) 6 } 7 }) 8 9 return ( 10 <reatomContext.Provider value={ctx}> 11 <Component {...pageProps} /> 12 </reatomContext.Provider> 13 ) 14}
Examples
- Migration from RTK to Reatom (2 times less code, -8kB gzip)
Setup batching for old React
For React 16 and 17 you need to setup batching by yourself in the root of your app.
For react-dom
:
1import { unstable_batchedUpdates } from 'react-dom' 2import { createCtx } from '@reatom/core' 3import { setupBatch, withBatching } from '@reatom/npm-react' 4 5setupBatch(unstable_batchedUpdates) 6const ctx = withBatching(createCtx())
For react-native
:
1import { unstable_batchedUpdates } from 'react-native' 2import { createCtx } from '@reatom/core' 3import { setupBatch } from '@reatom/npm-react' 4 5setupBatch(unstable_batchedUpdates) 6const ctx = withBatching(createCtx())
No vulnerabilities found.
Reason
30 commit(s) and 13 issue activity found in the last 90 days -- score normalized to 10
Reason
license file detected
Details
- Info: project has a license file: LICENSE.md:0
- Info: FSF or OSI recognized license: MIT License: LICENSE.md:0
Reason
no binaries found in the repo
Reason
dependency not pinned by hash detected -- score normalized to 6
Details
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/lint.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/artalar/reatom/lint.yml/v3?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/release-please.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/artalar/reatom/release-please.yml/v3?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:12: update your workflow using https://app.stepsecurity.io/secureworkflow/artalar/reatom/test.yml/v3?enable=pin
- Info: 0 out of 2 GitHub-owned GitHubAction dependencies pinned
- Info: 0 out of 1 third-party GitHubAction dependencies pinned
- Info: 2 out of 2 npmCommand dependencies pinned
Reason
8 existing vulnerabilities detected
Details
- Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275
- Warn: Project is vulnerable to: GHSA-mwcw-c2x4-8c55
- Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv
- Warn: Project is vulnerable to: GHSA-gcx4-mw62-g8wm
- Warn: Project is vulnerable to: GHSA-64vr-g452-qvp3
- Warn: Project is vulnerable to: GHSA-9cwx-2883-4wfx
- Warn: Project is vulnerable to: GHSA-8266-84wp-wv5c
- Warn: Project is vulnerable to: GHSA-5j4c-8p2g-v4jx
Reason
Found 3/23 approved changesets -- score normalized to 1
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
dangerous workflow patterns detected
Details
- Warn: script injection with untrusted input ' github.event.pull_request.head.ref ': .github/workflows/lint.yml:34
Reason
security policy file not detected
Details
- Warn: no security policy file detected
- Warn: no security file to analyze
- Warn: no security file to analyze
- Warn: no security file to analyze
Reason
detected GitHub workflow tokens with excessive permissions
Details
- Warn: no topLevel permission defined: .github/workflows/lint.yml:1
- Warn: no topLevel permission defined: .github/workflows/preview.yml:1
- Warn: topLevel 'contents' permission set to 'write': .github/workflows/release-please.yml:8
- Warn: no topLevel permission defined: .github/workflows/test.yml:1
- Info: no jobLevel write permissions found
Reason
project is not fuzzed
Details
- Warn: no fuzzer integrations found
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
- Warn: 0 commits out of 10 are checked with a SAST tool
Score
3.1
/10
Last Scanned on 2024-12-16
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More