Skip to content

Latest commit

 

History

History
147 lines (110 loc) · 3.65 KB

File metadata and controls

147 lines (110 loc) · 3.65 KB

Using React Hooks

Return to Table of Contents

Return to React

We're using React hooks for managing react state. Usually we separate hooks file to separate .state.ts file

State hook

As useState hook use Object.is to compare, it needs to pass new data everytime

So each hook should contain one state. If state uses as object, every change of the field, will trigger re-render of all components that depends on that state, so only if it used by one component at the time, then it make sense to use object.

Regular useState approach will have next look

component.state.ts

import { useState, useEffect } from 'react';

import { fetchDate } from './component.api';
import { formatUser, handleError } from './component.utils';

import { IUser } from './component.typings';

// Typing for our hook state data
interface IComponentStateData {
  users: IUser[];
  isLoading: boolean
}

// Typing for our hook return values
interface IComponentState extends IComponentStateData {
  error: string;
  selectedUser: IUser;
  onSelectUser: (user: IUser) => void;
}

const initialState = {
  users: [];
  isLoading: true,
}

// Custom hook
export const useComponentState = (id: string): IComponentState => {
  const [state, setState] = useState<IComponentStateData>(initialState);
  const [selectedUser, setSelectedUser] = useState<IUser | null>(null);
  const [error, setError] = useState<string>('');

  // Using instead componentDidMount
  useEffect(() => {
    const fetchUsers = async () => {
      try {
        const users = await fetchData(id);

        // Every setState we pass new object
        setState(currentState => ({
          ...currentState, // Just as example how to save previous state
          users: formatUsers(users),
          isLoading: false,
        }))
      } catch (e) {
        // As it update every field on state, no need to pass previous one
        setState(currentState => ({
          error: handleError(e),
          isLoading: false,
        }))
      }
    }

    fetchUsers();
  }, []);


  // Regular method that we can use for onClick
  const onSelectUser = (selectedUser: IUser) => {
    setSelectedUser(selectedUser)
  }

  // Returning data and needed method from hook
  return {
    ...state,
    error,
    selectedUser,
    onSelectUser
  }
}

And will use it in next way

component.tsx

import * as React from "react";

import { Spinner } from '@components/spinner";

import { useComponentState } from "./component.state";

import { IUser } from "./component.typings";

export interface IComponent {
  id: string;
}

export const Component = (props: IComponent) => {
  const { users, isLoading, users, selectedUser, onSelectUser } = useComponentState(props.id);

  if (isLoading) {
    return (<Spinner />)
  }
  /* ... */
};

Avoid using self-invoking functions

useEffect doesn't expect the callback to return promise, that's the reason why it's not allowed to make async useEffect. And solution to this, is create async / await functions. It can be done in different ways, but the one, which you need to avoid is creating self-invoking functions, better create functions inside hook or outside and just call it in useEffect:

Bad example

useEffect(() => {
  (async () => {
    const variable_name = await doSomething();
  })();
}, []);

Preferable way

const doSomething = async () => {
  const variable_name = await request();
  setState(variable_name);
};

useEffect(() => {
  doSomething();
}, []);

It works identically, but reads better and looks cleaner, if needed, we can reuse that functions later, for example in another hook.