Use Reactive in React
Step 1: Create a Store
Create a store with an initial state using the create API. It is recommended to create outside the component code for better code separation. For example, create and export in store.ts and put it in the same directory as your index.tsx component code for easy import.
Tips
store can be global or local, depending on your needs.
- If you need a global state, you can place the
store globally and then import it anywhere in your application.
- If you need a local state, you can create a
store within the component directory and then import it for use within the component to maintain the independence of the component logic.
- If neither scenario fits your situation, you may also consider using a lighter component-level Hooks solution useReactive.
Please ensure that the state within store is always a Pure Object, without functions, class instances, and other non-structural data. If needed, consider using ref.
store.ts
import { create } from '@shined/reactive'
// Create a store and specify the initial state, the state needs to be a `Pure Object`
export const store = create({
name: 'Bob',
info: {
age: 18,
hobbies: ['Swimming', 'Running'],
},
})
Step 2: Get Snapshot from Store
Inside the React Component
Use the useSnapshot Hook exposed by store to obtain a snapshot (snapshot) in the component and use it for rendering.
app.ts
import { store } from './store'
export default function App() {
// Use the snapshot in the store
const name = store.useSnapshot((s) => s.name)
return <div>{name}</div>
}
You can also pass in a selector function to manually specify the state you need to consume to optimize rendering, see Optional Rendering Optimization for details.
// All state changes in the store will trigger re-render
const snapshot = store.useSnapshot()
// Only re-render when `name` changes
const name = store.useSnapshot((s) => s.name)
// Only re-render when both `name` and `age` change
const [name, age] = store.useSnapshot((s) => [s.name, s.age] as const)
Tips
In complex state scenarios, for better code readability, you can also define some semantic Hooks within the adjacent store file for use, such as:
store.ts
// Define semantic Hooks
export const useName = () => store.useSnapshot((s) => s.name)
// Then use it in the component
function App() {
const name = useName()
return <div>{name}</div>
}
Outside the React Component
If you just need to read the state, you can directly read the store.mutate object while following the immutable principle.
// For basic data types, read directly
const userId = store.mutate.userId
// For reference types, create a derivative object based on the existing `store.mutate`, to follow the `immutable` principle
const namesToBeConsumed = store.mutate.list.map((item) => item.name);
The above method covers most scenarios. If you really need to get a snapshot outside the component, you can use store.snapshot().
// From version 0.2.0
const { name } = store.snapshot()
// Version 0.1.4 and earlier
import { getSnapshot } from '@shined/reactive'
const { name } = getSnapshot(store.mutate)
Step 3: Mutate the Store Anywhere
You can use store.mutate to change the state anywhere, Reactive will automatically trigger re-render.
Important
Reactive employs a read-write separation strategy. A snapshot (Snapshot) is considered a "snapshot state" of a certain stage and is non-expandable. You can only change the state by modifying the store.mutate object to generate a new snapshot, following the immutable design principle.
const info = store.useSnapshot((s) => s.info)
// Do not do this because the snapshot is read-only, you should change the state through `store.mutate`
info.name = 'Alice' // ❌
Inside the React Component
import { store } from './store'
export default function App() {
const name = store.useSnapshot((s) => s.name)
const updateName = () => store.mutate.name = 'Lily'
return (
<div>
<h1>Name: {name}</h1>
<button onClick={updateName}>Change Name</button>
</div>
)
}
Outside the React Component
You can also extract the logic for changing state to the store file for reuse.
store.ts
import { create, devtools } from '@shined/reactive'
export const store = create({
name: 'Bob',
data: null,
})
// Define a method to change the name
export const changeName = () => {
store.mutate.name = 'Squirtle'
}
// Define a method to fetch data
export const fetchData = async () => {
const data = await fetch('https://api.example.com/data')
store.mutate.data = await data.json()
}
Then use these methods in the component.
app.ts
import { useAsyncFn } from '@shined/react-use'
import { store, changeName, fetchData } from './store'
export default function App() {
const [name, data] = store.useSnapshot((s) => [s.name, s.data] as const)
the fetchDataFn = useAsyncFn(fetchData)
return (
<div>
<h1>Name: {name}, Data: {data}</h1>
<button onClick={changeName}>Change Name</button>
<button disabled={fetchDataFn.loading} onClick={fetchDataFn.run}>Fetch Data</button>
</div>
)
}
Step 4: Restore to Initial State
If needed, you can easily restore to the initial state through store.restore(), for example, resetting the state when the component unmounts.
import { useUnmount } from '@shined/react-use'
import { store } from './store'
export default function App() {
useUnmount(store.restore)
return (
<div>
<button onClick={store.restore}>Reset</button>
</div>
)
}