Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
January 12, 2023 07:31 pm GMT

Safe component state with useReducer and TypeScript

Photo by Nelly Antoniadou on Unsplash

Using multiple useState's to control component state is a common practice in React codebases, but it can result in unexpected behavior, such as a forever loading component or a disabled submit button.

Let's see what such components usually look like:

...const [email, setEmail] = useState('');const [loading, setLoading] = useState(false);const [error, setError] = useState<string | null>(null);const [success, setSuccess] = useState<string | null>(null);async function handleEmailSend() {  setLoading(true);  setSuccess(null);  setErrorMessage(null);  if (!isEmailValid(email)) {    setLoading(false);    return setErrorMessage('Invalid email');  }  try {    const response = await sendToAPI();    setLoading(false);    setSuccess(response);    setEmail('');  } catch (error) {    if (error instanceof Error) {      setErrorMessage(error.message);    }    setLoading(false);  }}

You can imagine that one missing setState can make an invalid UI state that we do not want.

We can use useReducer to make the code much cleaner and less error-prone.

Lets start by defining all the different possible component states we want to handle:

type Typing = { type: 'typing'; email: string };type Fetching = { type: 'fetching' };type FetchSuccess = { type: 'success'; message: string };type FetchFailed = { type: 'failed'; error: string };

We used type intersection to distinguish the correct type based on the type property. Our reducer state will be a union of all defined above types plus email filed.

type ResponseState = Typing | Fetching | FetchSuccess | FetchFailed;type BaseState = {  email: string;};type State = BaseState & ResponseState;

The reducer requires action types, which typically include a "type" property. We can utilize the valid UI states that we've already defined, eliminating the need for duplicating code when defining our actions.

type Action = Fetching | Typing | FetchSuccess | FetchFailed;function reducer(state: State, action: Action): State {  switch (action.type) {    case 'typing':    case 'fetching':    case 'failed': {      return { ...state, ...action };    }    case 'success': {      return { ...action, email: '' };    }  }}

The reducer is straightforward, thanks to the reuse of types from the reducer state for the actions, and the fact that their shape aligns with the reducer action pattern. The only exception is the success action, where we also clear the email input value. By connecting the reducer actions and state in this manner, unexpected states should no longer be an issue.

Let's see how we can simplify our handlers:

async function handleEmailSend() {  if (!isValidElement(state.email)) {    return dispatch({ type: 'failed', error: 'Invalid email'  });  }  try {    dispatch({ type: 'fetching' });    const response = await sendToAPI();    dispatch({ type: 'success', message: response });  } catch (error) {    if (error instanceof Error) {      dispatch({ type: 'failed', error: error.message });    }  }}

The component render is also pretty clear and easy to understand:

<div>  {state.type === 'failed' && <ErrorMessage error={state.error} />}  {state.type === 'success' && <Success message={state.message} />}</div>

The above example dismissed the invalid states and made our component safe, so we can assume that we archive the goal!

Please let me know in the comments what you think about that kind of reducer typing and if you have used it before!

If you want to explore the full example, here is the live example


Original Link: https://dev.to/czystyl/safe-component-state-with-usereducer-and-typescript-2m2f

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To