React(10) - useState & useReducer & useEffect


Posted by TempuraEngineer on 2022-11-04

目錄


React的hook

最常見的React hook是state hook和effect hook,它們都只能,也只該在function component的最上層(top level)使用

state hook(useState)讓function component能用state

effect hook(useEffect)則讓function component能用side effect。它可以做出componentDidMount、componentDidUpdate和componentWillUnmount的效果,但不推薦用class component的life cycle的方式去思考它們,因為它們事實上並不相同

在function component中useState、useEffect可以有多個,它們會依排放順序被執行

但如果state邏輯變得複雜,推薦推薦用reducer


useState

基本

useState()回傳兩個值,第一個是目前的state,第二個是用來變更state的function(setState function)

set function會將接收的值更新到state,並且重新渲染組件

另外需要注意function component的setState並不會像class component的會進行merge,它只會重新賦值

如果想要進行物件的merge則必須改寫一下

import {useState} from 'react';

import AddIcon from '@mui/icons-material/Add';

import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import Input from '@mui/material/Input';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';

function ToDoList_MUI(props){
  const [pendingValue, setPendingValue] = useState('');
  const [list, setList] = useState([
    {text:'sweeping floor', state:false},
  ]);

  const changePendingValue = (e) => {
    setPendingValue(() => e.target.value);
  }

  const addItem = () => {
    // 使用previous state的context進行merge
    setList((prev) => [...prev, {text:pendingValue, state:false}]);
    setPendingValue('');
  }

  const toggle = (e, index) => {
    const copiedList = JSON.parse(JSON.stringify(list));

    copiedList[index] = {text:list[index].text, state:e.target.checked}

    setList(copiedList);
  }

  return (
    <>
      <div className='flex'>
        <Input className='mr-2' value={pendingValue} onChange={changePendingValue}></Input>
        <Button variant="contained" startIcon={<AddIcon />} onClick={addItem}>add</Button>    
      </div>

      <Stack>
        {list.map((i, index) => {
          return <FormControlLabel 
                    key={index}
                    control={<Checkbox checked={i.state} disabled={i.state} onChange={(e) => {toggle(e, index)}} color='info'/>} 
                    label={i.text}
                    sx={{'& .MuiSvgIcon-root': { fontSize: 32 } }}/>
        })}
      </Stack>
    </>
  )
}

export default ToDoList_MUI;

另外setState只在初次渲染時將值初始化

資料更新使得DOM時的渲染只會讀取值,而不會再次初始化


多個useState

前段中有提到一個function component內可以有多個useState,但也可以使用useReducer代替

使用useReducer改寫

import {useReducer} from 'react';

import Toast from './Toast';

import AddIcon from '@mui/icons-material/Add';

import Input from '@mui/material/Input';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';

function reducer(state, action){
  const res = {...state}
  res[action.type] = action.value;

  return res;
}

function ToDoList_MUI(props){
  const [state, dispatch] = useReducer(reducer, {
    pendingValue: '',
    list:[
      {text:'sweeping floor', state:false},
    ],
  });

  const changePendingValue = (e) => {
    dispatch({type: 'pendingValue', value: e.target.value});
  }

  const addItem = () => {
    dispatch({type:'list', value:[...state.list, {text:state.pendingValue, state:false}]});
    dispatch({type: 'pendingValue', value: ''});
  }

  const toggle = (e, index) => {
    const copiedList = JSON.parse(JSON.stringify(state.list));

    copiedList[index] = {text:state.list[index].text, state:e.target.checked}

    dispatch({type: 'list', value: copiedList});
  }

  return (
    <>
      <div className='flex'>
        <Input className='mr-2' value={state.pendingValue} onChange={changePendingValue}></Input>
        <Button variant="contained" startIcon={<AddIcon />} onClick={addItem}>add</Button>    
      </div>

      <Stack>
        {state.list.map((i, index) => {
          return <FormControlLabel 
                    key={index}
                    control={<Checkbox checked={i.state} disabled={i.state} onChange={(e) => {toggle(e, index)}} color='info'/>} 
                    label={i.text}
                    sx={{'& .MuiSvgIcon-root': { fontSize: 32 } }}/>
        })}
      </Stack>
    </>
  )
}

export default ToDoList_MUI;


functional update

如果新的state需要根據舊的state來做計算,那可以傳一個函式給set function

set function收到的第一個參數是舊的state

function Calculator(props){
  const [count, setCount] = useState(0);
  const [operator, setOperator] = useState(undefined);

  const chooseNumber = (number) => {
    if(operator){
      calculate(number);

      return;
    }

    if(count === 0){
      setCount(number);

    }else{
      setCount((prev) => {
        const num = parseInt(prev.toString() + number.toString());

        setCount(num);
      })
    }
  }

  const calculate = (number) => {
    switch(operator){
      case '+':
        setCount((prev) => prev + number);
        break;

      case '-':
        setCount((prev) => prev - number);
        break;

      case '*':
        setCount((prev) => prev * number);
        break;

      case '/':
        setCount((prev) => prev / number);
        break;       

      default:
    }

    setOperator(undefined);
  }

  return (
    <div>
        <div className='bg-zinc-300 text-right mb-1 p-2 px-4'>{count}</div>

        <div className='grid grid-cols-3 gap-1'>
          <button className='bg-indigo-600 text-white p-2 px-4 col-span-3' onClick={() => setCount(0)}>clear</button>

          <button className='bg-blue-800 text-white p-2 px-4' onClick={() => setOperator('+')}>+</button>
          <button className='bg-blue-800 text-white p-2 px-4' onClick={() => setOperator('-')}>-</button>
          <button className='bg-blue-800 text-white p-2 px-4' onClick={() => setOperator('*')}>*</button>
          <button className='bg-blue-800 text-white p-2 px-4' onClick={() => setOperator('/')}>/</button>
          <button className='bg-blue-800 text-white p-2 px-4' onClick={() => {setCount((prev) => Math.pow(prev, 2))}}>x²</button>

          {
            [1, 2, 3, 4, 5, 6, 7, 8, 9, 0].map(i => 
                <button className='bg-blue-200 p-2 px-4' key={`button-${i}`} onClick={() => chooseNumber(i)}>{i}</button>
            )
          }
        </div>
    </div>
  )
}


useEffect

function component內預設是不允許side effect的,因為可能導致畫面和資料不一致

useEffect的第一個參數是function,當初次渲染、DOM被更新時會被執行。第二個參數則是dependency array,用於忽略傳入的callback function

side effect分為需要清除的、不需清除的2種

需要清除的是不清除可能導致memory leak(ex:使用package的API訂閱/監聽某些事件、計時器)

不須清除的,執行完之後就沒有了,不會留下潛在的爛攤子(ex:網路請求、手動變更DOM)


基本

function ToDoList_MUI(props){
  // 略

  useEffect(() => {
    if(state.list.length > 5){
      dispatch({type:'isWarningShow', value:true});
    }
  });

  // 略
}

Toast怎麼超過5就卡在那邊不關掉🤔

仔細看的話會發現useEffect不斷被呼叫,這是因為沒有設置dependency array,這會導致效能問題


忽略effect

使用dependency array解決useEffect不斷被呼叫的問題

React會對比新值與dependency array,若兩者值相同將忽略傳入的callback function

function ToDoList_MUI(props){
  // 略

    useEffect(() => {
    if(state.list.length > 5){
      dispatch({type:'isWarningShow', value:true});
    }

  }, [state.list.length]); // 相當於list更新時執行componentDidUpdate,若設為[]則相當於componentDidMount

  // 略
}

完整版

import {useReducer, useEffect} from 'react';

import Toast from './Toast';

import Input from '@mui/material/Input';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';

import AddIcon from '@mui/icons-material/Add';

function reducer(state, action){
  const res = {...state}
  res[action.type] = action.value;

  return res;
}

function ToDoList_MUI(props){
  const [state, dispatch] = useReducer(reducer, {
    pendingValue: '',
    list:[
      {text:'sweeping floor', state:false},
    ],
    isWarningShow:false,
    isErrorShow:false,
  });

  const changePendingValue = (e) => {
    dispatch({type: 'pendingValue', value: e.target.value});
    dispatch({type: 'isErrorShow', value: false});
  }

  const addItem = () => {
    if(!state.pendingValue){
      dispatch({type:'isErrorShow', value:true});
      return;
    };

    dispatch({type:'list', value:[...state.list, {text:state.pendingValue, state:false}]});
    dispatch({type: 'pendingValue', value: ''});
  }

  useEffect(() => {
    if(state.list.length > 5){
      dispatch({type:'isWarningShow', value:true});
    }

  });  // 相當於list更新時執行componentDidUpdate , [state.list.length]

  const toggle = (e, index) => {
    const copiedList = JSON.parse(JSON.stringify(state.list));

    copiedList[index] = {text:state.list[index].text, state:e.target.checked};

    dispatch({type: 'list', value: copiedList});
  }

  return (
    <>
      <div className='flex'>
        <Input className='mr-2' value={state.pendingValue} error={state.isErrorShow} onChange={changePendingValue}></Input>
        <Button variant="contained" startIcon={<AddIcon />} onClick={addItem}>add</Button>    
      </div>

      <Toast message='todo items > 5' isShow={state.isWarningShow} autoHide={800} variant='warning' onClose={() => {dispatch({type:'isWarningShow', value:false})}}></Toast>

      <Stack>
        {state.list.map((i, index) => {
          return <FormControlLabel 
                    key={index}
                    control={<Checkbox checked={i.state} disabled={i.state} onChange={(e) => {toggle(e, index)}} color='info'/>} 
                    label={i.text}
                    sx={{'& .MuiSvgIcon-root': { fontSize: 32 } }}/>
        })}
      </Stack>
    </>
  )
}

export default ToDoList_MUI;


清除side effect

useEffect預設在應用下一個side effect前,先清除舊的

在useEffect內回傳的函式會在組件被unmount時被呼叫(類似componentWillUnmount),因此可以用於清除舊的side effect

function Clock(props) {
  const [date, setDate] = useState(new Date().toLocaleString());

  useEffect(() => {
      const timer = setInterval(() => {
        setDate(new Date().toLocaleString());
      }, 1000);

      return () => {
        clearInterval(timer);
      }
  });

  return (
    <p>{date}</p>
  )
}


參考資料

React - Hook 概觀
React - Hook 的規則
React - Hooks API 參考
React - 使用 State Hook
React - 使用 Effect Hook
React - Functional updates
React - Component 生命週期
[React Hook 筆記] 從最基本的useState, useEffect 開始
React Hooks | 既生 useState 何生 useReducer ?
React warning Maximum update depth exceeded


#React #useState #useEffect #useReducer #Hooks







Related Posts

原子習慣:法則 3 - 讓行動輕而易舉

原子習慣:法則 3 - 讓行動輕而易舉

JavaScript變數&運算

JavaScript變數&運算

練習讓我們完成各種夢想 — 刻意練習

練習讓我們完成各種夢想 — 刻意練習


Comments