DEV Community

NDREAN
NDREAN

Posted on • Updated on

React with the state manager Valtio, some examples with fetch and SSE

Image description
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'
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

The custom hook named useSnapshot creates an immutable object from the state to pass to a React component:

const snap = useSnapshot(state)
Enter fullscreen mode Exit fullscreen mode

If we need only the field "index", we can destructure the snap:

const { index } = useSnapshot(state)
Enter fullscreen mode Exit fullscreen mode

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}/>
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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}</>
}
Enter fullscreen mode Exit fullscreen mode

and use it like this:

<Comp31 store={state}/>
<Comp32 store={state}/>
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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)),
});
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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}
    </>    
  )
}
Enter fullscreen mode Exit fullscreen mode

and use it:

<Comp6 store={commentState}/>
Enter fullscreen mode Exit fullscreen mode

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);
  },
});
Enter fullscreen mode Exit fullscreen mode

It remains to use this within a component:

const Comp6bis = ({ store }) => {
  const { derUsers } = useSnapshot(store);
  return <pre>{JSON.stringify(derUsers)}</pre>
};
Enter fullscreen mode Exit fullscreen mode

Since we are doing an async call, we need to suspend the component:

<React.Suspense fallback={'Loading...'}>
  <Comp6bis store={userState} />
</React.Suspense>
Enter fullscreen mode Exit fullscreen mode

This component will update whenever we change the value of the index.

Tip: namespace state.

Instead of:

const state = ({
  index: null,
  users: null
})
Enter fullscreen mode Exit fullscreen mode

use:

const state = ({
  index: { value: null },
  text: null
})
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

and the message is sent with the headers:

headers = {
  "Content-Type": "text/event-stream",
  Connection: "keep-alive",
};
Enter fullscreen mode Exit fullscreen mode

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}</>
}
Enter fullscreen mode Exit fullscreen mode

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
    )
  }
});
Enter fullscreen mode Exit fullscreen mode

where our state is:

const state = proxy({
  index: null,
  [...],
  sse: null,
})
Enter fullscreen mode Exit fullscreen mode

Our component will be:

const Comp7 = ({store}) => {
  const { sse } = useSnapshot(store);
  return <p>{sse}</p>;
}
Enter fullscreen mode Exit fullscreen mode

Use it:

  <Comp7 store={state}/>
Enter fullscreen mode Exit fullscreen mode

We implement a fake SSE emitter with Elixir here.

Oldest comments (0)