jtwang7 / React-Note

React 学习笔记
8 stars 2 forks source link

React - Hooks (useReducer) #8

Open jtwang7 opened 3 years ago

jtwang7 commented 3 years ago

React - Hooks (useReducer)

参考文章:

什么是 reducer?

简单来说,reducer 是一个函数,形如 (state, action) => newState,其接收当前组件的 state 和触发的动作 action,计算并返回最新的 state。其中 action 为一个对象类型,包含 type 和 payload 两个属性。

// 定义了一个 reducer,接收 state 和 action;
function countReducer(state, action) {
    // reducer 根据 action的类型对应修改 state
    switch(action.type) {
        case 'add':
            return state + 1;
        case 'sub':
            return state - 1;
        default: 
            return state;
    }
}

reducer 的使用重点:

immutable 在 React 中的必要性

action

action 用来表示触发 state 修改的行为。 action 是一个对象类型,包含两个属性:

const action = {
    // 行为名称
    type: 'addBook',
    // 负载参数
    payload: {
        book: {
            bookId,
            bookName,
            author,
        }
    }
}
function bookReducer(state, action) {
    switch(action.type) {
        case 'addBook':
            // 可通过 ES6 解构语法将部分参数单独解构使用
            const { book } = action.payload;
            return {
                ...state,
                books: {
                    ...state.books,
                    [book.bookId]: book,
                }
            };
        case 'sub':
            // ....
        default: 
            return state;
    }
}

reducer 总结

jtwang7 commented 3 years ago

React - Hooks (useReducer)

参考文章:

useReducer 使用场景

useReducer 与 useState 作用很像,都是 React Hooks 中用于状态声明和管理的方法。 useState 通常用于一些简单的状态管理。而 useReducer 主要有以下应用场景:

useReducer

const [state, dispatch] = useReducer(reducer, initState, init);

state 初始化

dispatch

dispatch 接收一个对象作为 action,并执行 useReducer 中相应的 reducer 函数,reducer 函数中 state 参数取自 useReducer 持有的 state 变量,action 参数取自 dispatch 接收的 action 对象。dispatch 执行结果的的返回值用于替换原有的 state 变量。

const [myState, dispatch] = useReducer(reducer, init);
// dispatch 流程
// dispatch(myAction) -> reducer(myState, myAction) & return newState -> myState = newState

dispatch 特点

React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。 这一特点在 React 性能优化中有着许多应用。例如 props 传递回调函数时,由于每次 render 都会导致回调函数重新定义,引发子组件的 rerender,传递 dispatch 可以避免该问题。此外,dispatch 还可以用于避免 useEffect 某个 state 频繁更新导致多次渲染的问题。

useState 版 login 实现

function LoginPage() {
    // 可以看到 login 需要维护许多 state,而这些 state 又是相关的。
    // 如果用 useState,则需要定义多个分散的 state,且 setState 会被分散在代码各处,不好维护。
    const [name, setName] = useState(''); // 用户名
    const [pwd, setPwd] = useState(''); // 密码
    const [isLoading, setIsLoading] = useState(false); // 是否展示loading,发送请求中
    const [error, setError] = useState(''); // 错误信息
    const [isLoggedIn, setIsLoggedIn] = useState(false); // 是否登录

    const login = (event) => {
        event.preventDefault();
        setError('');
        setIsLoading(true);
        login({ name, pwd })
            .then(() => {
                setIsLoggedIn(true);
                setIsLoading(false);
            })
            .catch((error) => {
                // 登录失败: 显示错误信息、清空输入框用户名、密码、清除loading标识
                setError(error.message);
                setName('');
                setPwd('');
                setIsLoading(false);
            });
    }
    return ( 
        //  返回页面JSX Element
    )
}

useState 在复杂 state 管理中的一些问题

useReducer 版的 login 实现

// useReducer 的第二参数:初始化 state
const initState = {
    name: '',
    pwd: '',
    isLoading: false,
    error: '',
    isLoggedIn: false,
}
// useReducer 的第一参数:定义了各个 actionType 对应的 state 修改行为
function loginReducer(state, action) {
    switch(action.type) {
        case 'login':
            return {
                ...state,
                isLoading: true,
                error: '',
            }
        case 'success':
            return {
                ...state,
                isLoggedIn: true,
                isLoading: false,
            }
        case 'error':
            return {
                ...state,
                error: action.payload.error,
                name: '',
                pwd: '',
                isLoading: false,
            }
        default: 
            return state;
    }
}
function LoginPage() {
    // 这里仅用一次 useReducer 就代替了上述多个 useState 定义
    const [state, dispatch] = useReducer(loginReducer, initState);
    // 对象解构
    const { name, pwd, isLoading, error, isLoggedIn } = state;
    const login = (event) => {
        event.preventDefault();
        // 告知 dispatch 行为类型,就会执行事先定义好的逻辑,返回新的 state 对象。
        dispatch({ type: 'login' });
        login({ name, pwd })
            .then(() => {
                dispatch({ type: 'success' });
            })
            .catch((error) => {
                dispatch({
                    type: 'error'
                    payload: { error: error.message }
                });
            });
    }
    return ( 
        //  返回页面JSX Element
    )
}

对比发现,useReducer 将 state 和 setState 操作都统一到了一起,在使用时,我们只需要调用 dispatch 指定对应的操作类型,不需要关心 reducer 是如何处理 state 的,我们看到的就是 state 输入的值以及 state 输出的结果。此外,useReducer 将 state 的处理逻辑都封装到了 dispatch 函数内,使得我们复用 state 的逻辑更加容易,比如在其他函数中也需要触发登录 error 状态,只需要 dispatch({ type: 'error' })

jtwang7 commented 3 years ago

React - Hooks (useReducer)

参考文章:

前言

在上述两篇中,讲解了 reducer 的基础概念以及 useReducer 的基本使用。 useReducer 可以帮助我们集中式的处理复杂的state管理,此外 useReducer 另一大用处就是能够解决深层子组件的状态修改。 由于 React 是单向数据流传递,因此对于 state 在深层次的组件状态修改,往往需要通过组件树中多次传递回调函数来解决,通过 props 添加回调层层传递的方法存在以下问题:

context / useContext

context 作用就是对它所包含的组件树提供全局共享数据。 useContext 为函数组件提供了获取 consumer 的 Hook。

// 第一步:创建需要共享的context
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // 第二步:使用 Provider 提供 ThemeContext 的值,Provider所包含的子树都可以直接访问ThemeContext的值
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}
// Toolbar 组件并不需要透传 ThemeContext
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton(props) {
  // 第三步:使用共享 Context
  const theme = useContext(ThemeContext);
  render() {
    return <Button theme={theme} />;
  }
}
  1. 创建 Context 对象 const MyContext = React.createContext(defaultValue); 当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。 只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。

    注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。

  2. Context.Provider 包裹子级消费者 <MyContext.Provider value={/* 某个值 */}>消费组件</MyContext.Provider> 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。

    通过新旧值检测来确定变化,使用了与 Object.is 相同的算法。

  3. useContext 使用共享 Context const value = useContext(MyContext); 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 的 value prop 决定。

    调用了 useContext 的组件(消费组件)总会在 context 值变化时重新渲染。也就是说,当Context Provider 的 value 发生变化时,他的所有子级消费者都会rerender。

注意!!!若定义 Context 对象的组件和使用 Context 对象的组件位于不同的 JS 文件(通常都是这种情况),你需要将定义的 Context 对象导出,并在使用的组件中 import 导入。 组件导出使用的是 export default,因此我们还可以用 export 导出 Context 对象的引用。


// App.js
// export 导出 Context 对象
export const ThemeContext = React.createContext('light');

// 组件是通过 export default 导出的 export default class App extends React.Component { render() { // 第二步:使用 Provider 提供 ThemeContext 的值,Provider所包含的子树都可以直接访问ThemeContext的值 return (

); } } ``` > 当然,你也可以将 Context 对象创建单独放在一个 js 文件中,并同时在包含 `Provider` 和 `Consumer` 的 js 文件中引入。事实证明,该方法是最优的方法,若将 Context 对象声明在 `Provider` 所在的 js 文件中,`Consumer` 在导入时可能会出错,原因不明。 ```js // ThemeButton.js // 只需要导入相应的 Context 对象即可 import {ThemeContext} from './App.js' export default function ThemedButton(props) { // 使用共享 Context 对象 const theme = useContext(ThemeContext); render() { return