We review some basic examples of how to use the state management library Valtio in combination with React.
The gist of this library is to let us subscribe to a proxied state via a "snapshot" hook in a React component (but not limited to). When we return the "snap" from the component, any mutation of the proxied state will make the component render. The rule is: read only from snap
, and write only to the state. Furthermore, actions are idempotent, so no useless rendering.
import { proxy, useSnapshot } from 'valtio'
import { derive } from 'valtio/utils'
Firstly, we wrap a state with proxy
. There can be many states. For example here, we will consider the following state:
const state = proxy({
index: 1,
text: null,
message: null
})
The custom hook named useSnapshot
creates an immutable object from the state to pass to a React component:
const snap = useSnapshot(state)
If we need only the field "index", we can destructure the snap:
const { index } = useSnapshot(state)
Example 1: not reactive
This component is not reactive since we are reading from a mutable object, the state.
const Comp1 = ({store}) => <pre>{JSON.stringify(store)}</pre>
<Comp1 store={state}/>
Instead, do:
Example 2: reactive, read from snap.
This component is reactive because we are reading from the snap, an immutable object so changes to the state will be captured.
const Comp2 = ({store}) => {
const snap useSnapshot(store)
return <pre>{JSON.stringify(snap)}</pre>
}
Example 3: "atomize" your components
To limit rendering, "atomize" components
const Comp31 = ({store}) => {
const {index} = useSnapshot(store)
return <>{index}</>
}
const Comp32 = ({store}) => {
const {text} = useSnapshot(store)
return <>{text}</>
}
and use it like this:
<Comp31 store={state}/>
<Comp32 store={state}/>
The first component will render if we change the field "index" in the state, but will not render when the field "text" is changed and vice-versa.
Example 4: write to state, read from snap, again
Write to the state - so mutate it - and read from the snap. In particular, use state in callbacks, not snaps.
const Comp4 = ({ store }) => {
const { index } = useSnapshot(store);
return (
<p>
<button onClick={() => ++store.index}>
Increment
</button>
{" "}{index}
</p>
);
};
Example 5: mutate the state, display with snap, again.
We mutate the state and display some modifications of the snap.
const double = nb => nb * 2
const useTriple = (store) => {
const index = useSnapshot(store)
return index * 2
}
const Comp5 = ({store}) => {
const { index } = useSnapshot(store)
const triple = useTriple(store)
return(
<>
<button onClick={() => ++store.index}>{" "}
{index}{" "}{double(index)}{" "}{triple}
</>
)
}
This will update the state on each click and render it plus some computations.
Example 6: reactive to fetch
data
Suppose we have to populate a field with an api. For example, fetch a list of "users" under a certain index
from a backend. If we are on the same page with this component to populate, for example when we select it, we would use useEffect
and update our local state to render the component.
We will use Valtio below for the same purpose.
Consider the state below:
export const commentState = proxy({
comments: null,
setComments: async () =>
(comments.comments = await fetchComments(store.index.value)),
});
and a utility "fetching" function that could be something like:
export const fetchComments = async (id) => {
const data = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}/comments`);
return data.json();
};
We can define an action button in our component that will trigger the fetch, subscribe with a snapshot to the state "comments" and use it to render:
const Comp6 = ({store}) => {
const { comments } = useSnapshot(store)
return(
<>
<button onClick={()=> commentState.setComments()}>Fetch</button>
{comments}
</>
)
}
and use it:
<Comp6 store={commentState}/>
We now want our component to be reactive to "external" proxied changes, i.e. changes not triggered within the component (as the previous button) but from another component. Again we will rely on a state mutation. For example, suppose that we selected an "index" which is captured in the state "state" (our first one). We introduce a "derivation" on the state "state" via a get
:
export const userState = derive({
derUsers: async (get) => {
const list = await fetchComments(get(state.index).value);
return list?.map((c) => c.email);
},
});
It remains to use this within a component:
const Comp6bis = ({ store }) => {
const { derUsers } = useSnapshot(store);
return <pre>{JSON.stringify(derUsers)}</pre>
};
Since we are doing an async call, we need to suspend the component:
<React.Suspense fallback={'Loading...'}>
<Comp6bis store={userState} />
</React.Suspense>
This component will update whenever we change the value of the index.
Tip: namespace state.
Instead of:
const state = ({
index: null,
users: null
})
use:
const state = ({
index: { value: null },
text: null
})
The idea is that you can use get(state.index).value
and limit interaction or undesired rendering.
Example 7: with SSE
to external events
We take this example as it needs less set-up than websockets. Suppose that the backend or an API is sending Server Sent Events to the front-end. The SSE server pushes data over HTTP in the form of a stream (where the default event type is "message"):
"event: message \n data: xxx\n id: uuid4\n\n"
and the message is sent with the headers:
headers = {
"Content-Type": "text/event-stream",
Connection: "keep-alive",
};
Then we implement a Javascript function that uses the Server-Sent-Event interface with a listener to SSE events.
We can handle this within a useEffect
:
const Comp6 = () => {
const [msg, setMsg] = React.useState(null);
const handleMsg = (e) => {
setMsg(e.data)
}
React.useEffect(() => {
const source = new EventSource(process.env.REACT_APP_SSE_URL);
source.addEventListener('message', (e) => handleMsg(e)
return () => {
source.removeEventListener("message", handleMsg);
source.close()
};
}, []);
return <>{msg}</>
}
We can do the same with Valtio using derive
. We build a derivation from the state "state" that saves the content of the messages to the state "state":
export const sseEvents = derive({
getMsg: (get) => {
const evtSource = new EventSource('http://localhost:4000/sse');
evtSource.addEventListener('message', (e) =>
get(state).sse = e.data
)
}
});
where our state is:
const state = proxy({
index: null,
[...],
sse: null,
})
Our component will be:
const Comp7 = ({store}) => {
const { sse } = useSnapshot(store);
return <p>{sse}</p>;
}
Use it:
<Comp7 store={state}/>
We implement a fake SSE emitter with Elixir here.
Oldest comments (0)