5 React Useful Tips for Beginners

SeongKuk Han - Feb 17 '22 - - Dev Community

When I first started learning React, I had a hard time understanding its concepts. I still do, but as we all learn from our struggles, I did learn some very useful tips.
In this post I'm gonna share few tips which I've learned over the time when I was learning React.

Let's get started!

1. Event Handler With Closure

Before

type State = { name: string; number: string };

function App() {
  const [state, setState] = useState<State>({
    name: '',
    number: '',
  });

  const handleNameChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const name = e.target.value.substring(0, 20);

      setState((prevState) => ({
        ...prevState,
        name,
      }));
    },
    []
  );

  const handleNumberChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const number =
        e.target.value
          .substring(0, 20)
          .match(/[0-9]|-/g)
          ?.join('') || '';

      setState((prevState) => ({
        ...prevState,
        number,
      }));
    },
    []
  );

  return (
    <div>
      <span>Name</span>
      <input type="text" onChange={handleNameChange} value={state.name} />
      <span>Phone</span>
      <input type="text" onChange={handleNumberChange} value={state.number} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

After

type State = { name: string; number: string };
type StateKey = keyof State;

type HandleStateChangeOption = { maxLength?: number; useTelFormat?: boolean };

function App() {
  const [state, setState] = useState<State>({
    name: '',
    number: '',
  });

  const handleStateChange = useCallback(
    (key: StateKey, options: HandleStateChangeOption) =>
      (e: React.ChangeEvent<HTMLInputElement>) => {
        let { value } = e.target;

        if (options.useTelFormat) {
          value = value.match(/[0-9]|-/g)?.join('') || '';
        }

        if (options.maxLength !== undefined) {
          value = value.substring(0, options.maxLength);
        }

        setState((prevState) => ({
          ...prevState,
          [key]: value,
        }));
      },
    []
  );

  return (
    <div>
      <span>Name</span>
      <input
        type="text"
        onChange={handleStateChange('name', { maxLength: 20 })}
        value={state.name}
      />
      <span>Phone</span>
      <input
        type="text"
        onChange={handleStateChange('number', {
          useTelFormat: true,
          maxLength: 20,
        })}
        value={state.number}
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now, you can control several states in a place and use the options.

2. Functional Components

The React functional components? No, Not. It means the components that do something without UI.

Before

function App() {
  const [enableScroll, setEnableScroll] = useState(true);
  const [prevBodyOverflow, setPrevBodyOverflow] = useState<string>('auto');

  const handleButtonClick = useCallback(() => {
    if (enableScroll) {
      setPrevBodyOverflow(document.body.style.overflow);
      document.body.style.overflow = 'hidden';
    } else {
      document.body.style.overflow = prevBodyOverflow;
    }
    setEnableScroll(!enableScroll);
  }, [enableScroll, prevBodyOverflow]);

  const buttonText = useMemo(
    () => (enableScroll ? 'disable scroll' : 'enable scroll'),
    [enableScroll]
  );

  return (
    <div style={{ height: '200vh', backgroundColor: 'gray' }}>
      <button onClick={handleButtonClick}>{buttonText}</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

After

function DisableScroll() {
  useEffect(() => {
    const prevBodyOverflow = document.body.style.overflow;

    document.body.style.overflow = 'hidden';
    return () => {
      document.body.style.overflow = prevBodyOverflow;
    };
  }, []);

  return null;
}

function App() {
  const [enableScroll, setEnableScroll] = useState(true);

  const handleButtonClick = useCallback(() => {
    setEnableScroll(!enableScroll);
  }, [enableScroll]);

  const buttonText = useMemo(
    () => (enableScroll ? 'disable scroll' : 'enable scroll'),
    [enableScroll]
  );

  return (
    <div style={{ height: '200vh', backgroundColor: 'gray' }}>
      {!enableScroll && <DisableScroll />}
      <button onClick={handleButtonClick}>{buttonText}</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Before

function App() {
  const [count, setCount] = useState(60);

  useEffect(() => {
    const tm = setInterval(() => {
      setCount((count) => {
        if (count === 1) {
          clearInterval(tm);
        }

        return count - 1;
      });
    }, 1000);
  }, []);

  return (
    <div>
      <span>{count}</span>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

After

interface ICountProps {
  onCount: (count: number) => void;
  startNumber: number;
}

function Count({ onCount, startNumber }: ICountProps) {
  const [count, setCount] = useState(startNumber);

  useEffect(() => {
    const tm = setInterval(() => {
      setCount((count) => {
        if (count === 1) {
          clearInterval(tm);
        }

        return count - 1;
      });
    }, 1000);
  }, []);

  useEffect(() => {
    onCount(count);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [count]);

  return null;
}

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <Count startNumber={60} onCount={(count) => setCount(count)} />
      <span>{count}</span>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

I'm not sure it's a good example though. I believe you'll find the right case.
Replace your js code to components in a React way.

3. Separating map from 'return' and use it with useMemo

before

type ItemType = { id: number; name: string; price: string };

type StateChangeHandler = (
  state: keyof ItemType
) => (e: React.ChangeEvent<HTMLInputElement>) => void;

interface IItemProps extends ItemType {
  onChange: StateChangeHandler;
  onRemove: VoidFunction;
}

function Item({ id, name, price, onChange, onRemove }: IItemProps) {
  return (
    <div>
      <span>ID: {id}</span>
      <span>name</span>
      <input type="text" value={name} onChange={onChange('name')} />
      <span>price</span>
      <input type="text" value={price} onChange={onChange('price')} />
      <button onClick={onRemove}>REMOVE</button>
    </div>
  );
}

const newItem = (id: number): ItemType => ({ id, name: 'Item', price: '10' });
const initItems = (): ItemType[] => [newItem(1)];

function App() {
  const [items, setItems] = useState<ItemType[]>(initItems());

  const handleItemAdd = useCallback(() => {
    setItems(items.concat(newItem(items[items.length - 1].id + 1)));
  }, [items]);

  const handleItemRemove = useCallback(
    (index: number) => () => {
      const newItems = [...items];
      newItems.splice(index, 1);
      setItems(newItems);
    },
    [items]
  );

  const handleStateChange = useCallback(
    (index: number) =>
      (state: keyof ItemType) =>
      (e: React.ChangeEvent<HTMLInputElement>) => {
        const newItems = [...items];

        switch (state) {
          case 'id':
            newItems[index][state] = parseInt(e.target.value, 10);
            break;
          default:
            newItems[index][state] = e.target.value;
        }

        setItems(newItems);
      },
    [items]
  );

  return (
    <div>
      <button onClick={handleItemAdd}>ADD</button>
      <br />
      {items.map((item, itemIdx) => (
        <Item
          key={itemIdx}
          {...item}
          onChange={handleStateChange(itemIdx)}
          onRemove={handleItemRemove(itemIdx)}
        />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

After

...
const itemComponents = useMemo(() => {
    return items.map((item, itemIdx) => (
      <Item
        key={itemIdx}
        {...item}
        onChange={handleStateChange(itemIdx)}
        onRemove={handleItemRemove(itemIdx)}
      />
    ));
  }, [handleItemRemove, handleStateChange, items]);

  return (
    <div>
      <button onClick={handleItemAdd}>ADD</button>
      <br />
      {itemComponents}
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

It looks focus more First thing.
But the point is keeping your return clean.

4. Conditional Rendering with Compound Components

Before

function App() {
  const [number, setNumber] = useState('');

  const handleNumberChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) =>
      setNumber(e.target.value.match(/[0-9]/g)?.join('') || ''),
    []
  );

  const realNum = Number(number);

  return (
    <div>
      <input type="text" value={number} onChange={handleNumberChange} />
      <br />
      {realNum < 10 && <div>number is less than 10.</div>}
      {realNum > 10 && <div>number is greater than 10.</div>}
      {realNum < 5 && <div>number is less than 5.</div>}
      {realNum > 3 && <div>number is greater than 3.</div>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

After

interface IContext {
  contextNum: number;
}

interface INumComponentProps {
  num: number;
  children?: React.ReactNode;
}

const CondtionalRenderingContext = React.createContext<IContext>({
  contextNum: 0,
});

const useContextNum = () => React.useContext(CondtionalRenderingContext);

function CondtionalRendering({ num, children }: INumComponentProps) {
  return (
    <CondtionalRenderingContext.Provider value={{ contextNum: num }}>
      {children}
    </CondtionalRenderingContext.Provider>
  );
}

function Over({ num, children }: INumComponentProps) {
  const { contextNum } = useContextNum();
  if (contextNum <= num) return null;

  return <>{children}</>;
}

function Under({ num, children }: INumComponentProps) {
  const { contextNum } = useContextNum();
  if (contextNum >= num) return null;

  return <>{children}</>;
}

function App() {
  const [number, setNumber] = useState('');

  const handleNumberChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) =>
      setNumber(e.target.value.match(/[0-9]/g)?.join('') || ''),
    []
  );

  const realNum = Number(number);

  return (
    <div>
      <input type="text" value={number} onChange={handleNumberChange} />
      <br />
      <CondtionalRendering num={realNum}>
        <Under num={10}>
          <div>number is less than 10.</div>
        </Under>
        <Over num={10}>
          <div>number is greater than 10.</div>
        </Over>
        <Under num={5}>
          <div>number is less than 5.</div>
        </Under>
        <Over num={3}>
          <div>number is greater than 3.</div>
        </Over>
      </CondtionalRendering>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

It's not always a better way. There are more components, it can be not good in performance. Consider it whether the benefits are better than the shortcoming or not in your code. (Like readability)

5. Custom Dialogs with context

Before

interface IDialog {
  message: string;
  onConfirm?: VoidFunction;
}

function Dialog({ message, onConfirm }: IDialog) {
  return (
    <ModalOverlay>
      <ModalContainer>
        <p>{message}</p>
        <button onClick={onConfirm}>OK</button>
      </ModalContainer>
    </ModalOverlay>
  );
}

function App() {
  const [text, setText] = useState('');
  const [dialog, setDialog] = useState<IDialog | undefined>(undefined);

  const handleDialogOpen = useCallback(() => {
    setDialog({
      message: text,
      onConfirm: () => {
        setText('');
        setDialog(undefined);
      },
    });
  }, [text]);

  return (
    <div>
      {dialog && <Dialog {...dialog} />}
      <input
        type="text"
        onChange={(e) => setText(e.target.value)}
        value={text}
      />
      <button onClick={handleDialogOpen}>Click Me!</button>
    </div>
  );
}

const ModalOverlay = styled.div`
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background-color: rgba(0, 0, 0, 0.4);
  display: flex;
  justify-content: center;
  align-items: center;
`;

const ModalContainer = styled.div`
  min-width: 300px;
  padding: 24px;
  background-color: white;
  box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
  border-radius: 12px;

  > button {
    width: 100%;
  }
`;
Enter fullscreen mode Exit fullscreen mode

After

interface IDialog {
  message: string;
  onConfirm?: VoidFunction;
}

interface IDialogContext {
  showDialog: (paramaeters: IDialog) => void;
}

const DialogContext = React.createContext<IDialogContext>(undefined!);

const useDialog = () => React.useContext(DialogContext);

function DialogContextProvider({ children }: { children?: React.ReactNode }) {
  const [dialog, setDialog] = useState<IDialog | undefined>(undefined);

  const showDialog = useCallback(({ message, onConfirm }: IDialog) => {
    setDialog({
      message,
      onConfirm: () => {
        onConfirm && onConfirm();
        setDialog(undefined);
      },
    });
  }, []);

  return (
    <DialogContext.Provider value={{ showDialog }}>
      {dialog && <Dialog {...dialog} />}
      {children}
    </DialogContext.Provider>
  );
}

function Dialog({ message, onConfirm }: IDialog) {
  return (
    <ModalOverlay>
      <ModalContainer>
        <p>{message}</p>
        <button onClick={onConfirm}>OK</button>
      </ModalContainer>
    </ModalOverlay>
  );
}

function Page() {
  const [text, setText] = useState('');
  const { showDialog } = useDialog();

  const handleDialogOpen = useCallback(() => {
    showDialog({
      message: text,
      onConfirm: () => setText(''),
    });
  }, [text]);

  return (
    <div>
      <input
        type="text"
        onChange={(e) => setText(e.target.value)}
        value={text}
      />
      <button onClick={handleDialogOpen}>Click Me!</button>
    </div>
  );
}

function App() {
  return (
    <DialogContextProvider>
      <Page />
    </DialogContextProvider>
  );
}

const ModalOverlay = styled.div`
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background-color: rgba(0, 0, 0, 0.4);
  display: flex;
  justify-content: center;
  align-items: center;
`;

const ModalContainer = styled.div`
  min-width: 300px;
  padding: 24px;
  background-color: white;
  box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
  border-radius: 12px;

  > button {
    width: 100%;
  }
`;
Enter fullscreen mode Exit fullscreen mode

I made the page component for avoiding confusing where you should put the Provider. You don't have to take care all of the dialog code. Just get the show method from useDialog anywhere then use it.

showDialog({ message: 'hi!' });

Also, you can make other dialogs and modals like this way.


Conclusion

Here are 5 React useful tips. I hope the post helps someone. If you have better examples with these tips or if you have your useful tips, please let me know in the comment below.

Thank you for reading this and happy React!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .