skip to content

Search

React hooks 入门

17 min read

React hooks 简明入门教程

React hooks 入门

此教程翻译自Academind课程, 源地址:https://www.youtube.com/watch?v=-MlNBTSg_Ww

为何需要React Hooks

为了状态和生命周期而去写Class组件往往是繁琐的, 而且组合优于继承的原则, 使得在多数情况下能定义函数组件就不去定义类组件 在新版React(16.8)中, 新增了React Hooks, 使我们可以在函数组件中使用状态和生命周期函数, 使得函数组件更加灵活, 减轻类组件使用负担


从state开始(useState)

项目目录:
src/
	|-components/
  |---TabList.js
  |---Schedule.js
  |-hooks/
  |---http.js

首先我们声明一个无状态函数组件, 内容如下:

const TabList = () => {
  const currentTab = 1;
  return <div>CurrentTab: {currentTab}</div>;
};

这是一个普通的无状态组件, 我们使用新的useState API来赋予初始值: (useState的调用参数和state一样, 可赋予任何值)(useState的参数为state的初始值) useState方法返回1个数组, 数组内包含2个参数, 第一个参数是当前state:

import React, { useState } from "react";
 
const TabList = () => {
  const [state] = useState({
    selectedTab: 1,
  });
  return <div>CurrentTab: {state.selectedTab}</div>;
};

useState方法返回1个数组, 第二个参数是改变状态函数(此处暂时理解为setState, 但≠setState)

const TabList = () => {
  const [tabState, setTabState] = useState(1);
 
  const changeTabState = () => {
    setTabState(tableState + 1);
  };
 
  return (
    <div>
      CurrentTab: {tabState}
      <button onClick={changeTabState}>Just CurrentTab + 1</button>
    </div>
  );
};

刷新浏览器即可看到setTableState’模仿’了class组件中的setState方法. 为什么说模仿呢 ? 我们修改下useState的参数, 现在传递一个多属性的对象值, 如下:

const TabList = () => {
  const [tabState, setTabState] = useState({
    selectedTab: 1,
    text: "initValue",
  });
 
  const changeTabState = () => {
    setTabState({
      selectedTab: tabState.selectedTab + 1,
    });
  };
 
  return (
    <div>
      <div>CurrentTab: {tabState.selectedTab}</div>
      <div>
        <button onClick={changeTabState}>Just CurrentTab + 1</button>
      </div>
      CurrentText: {tabState.text}
    </div>
  );
};

刷新浏览器: 第一次text的state加载正常, 我们点击按钮, text消失了???

useState引发的问题

从上段代码中, 我们可以发现useState返回的参数中, 使用修改状态的函数时候, 跟class中的setState并不一致. 在class中, 我们每次使用setState进行改变状态的时候, React做出的操作是mergeData, 我们简单理解为Object.assign方法, 而在useState返回值中, 我们修改是直接replaceData, 是直接替换了状态, 替换了新的值上去, 所以, 上述代码中text的值在更新状态后丢失了, 导致渲染空数据.

useState问题的解决方案

手动mergeData

就像redux那样, 我们每次返回值的时候把上一次的值再传递过去, 可以很快的解决旧数据丢失问题:

const changeTabState = () => {
  setTabState({
    ...tabState,
    selectedTab: tabState.selectedTab + 1,
  });
};

定义多个state

在class中, 我们有且仅有一个state是因为state是作为class的属性而存在的, 而在函数组件中, useState是一个方法, 我们不难想到, 可以定义多个state去分别更新不同的state, 这也是React的灵活设计方式:

const TabList = () => {
  const [tabState, setTabState] = useState(1);
  const [textState, setTextState] = useState("initial value");
 
  const changeTabState = () => {
    setTabState(tabState + 1);
  };
 
  const changeTextState = () => {
    setTextState("new value");
  };
 
  return (
    <React.Fragment>
      <div>CurrentTab: {tabState}</div>
      <div>
        <button onClick={changeTabState}>Just CurrentTab + 1</button>
      </div>
      <div>CurrentText: {textState}</div>
      <div>
        <button onClick={changeTextState}>set new Text Value</button>
      </div>
    </React.Fragment>
  );
};

生命周期函数useEffect

在React hooks中, 所有生命周期函数都将由useEffect这一个API实现

componentDidMount

我们通常在componentDidMount函数中发起网络请求, 确认接收到参数再渲染数据. 我们使用useEffect函数来实现此场景: useEffect是一个函数, 可接收一个方法 我们定义一个新的组件作为TabList的子组件, 来测试是否还原了’componentDidMount’方法:

const TabList = () => {
  const [clickTimeState, setClickTimeState] = useState(0);
 
  const changeClickTime = () => {
    setClickTimeState(clickTimeState + 1);
  };
 
  return (
    <React.Fragment>
      <div>ClickTime: {clickTimeState}</div>
      <div>
        <button onClick={changeClickTime}>Just clickTime + 1</button>
      </div>
      <Schedule clickTime={clickTimeState} />
    </React.Fragment>
  );
};
import React, { useEffect } from "react";
 
const Schedule = ({ clickTime }) => {
  useEffect(() => {
    console.log("clickTime", clickTime);
    console.log("useEffect works");
  });
 
  return <React.Fragment>This is Schedule</React.Fragment>;
};

我们多次点击按钮, 发现每次都执行了useEffect中的代码, 现在, 证明了一点: useEffect中的函数在每次props改变都会执行. 在componentDidMount中, 自己状态改变只会执行一次, 如果组件内状态改变了, 但是useEffect中的函数代码没有执行, 就证明我们’还原’了componentDidMount这个API. 我们现在模拟一个网络请求来修改组件内状态:

const actions = ["eat", "sleep"];
 
let pointer = 0;
 
const newScheduleList = [];
for (let i = 0; i < 10; i++) {
  if (actions[pointer]) {
    newScheduleList.push({
      id: i,
      action: actions[pointer],
    });
  } else {
    newScheduleList.push({
      id: i,
      action: actions[0],
    });
    pointer = 0;
  }
  pointer++;
}
 
// clickTime后续会用到
const Schedule = ({ clickTime }) => {
  const [loading, setLoading] = useState(false);
  const [scheduleList, setScheduleList] = useState([]);
 
  useEffect(() => {
    setLoading(true);
    console.log("useEffect works");
    setTimeout(() => {
      setLoading(false);
      setScheduleList(newScheduleList);
    }, 1000);
  });
 
  return (
    <React.Fragment>
      {loading ? (
        "loading..."
      ) : (
        <ul>
          {scheduleList.map((item) => (
            <li key={item.id}>
              {item.id} -- {item.action}
            </li>
          ))}
        </ul>
      )}
    </React.Fragment>
  );
};

    刷新浏览器, 打开控制台, 我们发现进入了一个死循环, useEffect内的函数在无限制执行

    分析后不难发现, useEffect好像在每次componentDidUpdate执行, props不变的情况下, 我们修改了setLoading, 此时state变化, 第二次进入useEffect内函数代码, 所以陷入了一个死循环

useEffect接受2个参数, 第一个为函数, 第二个为数组, 第二个参数用来监听dependencies, 每次数组内的值变 化, 触发第一个函数, 默认监听所有dependencies, 第一次render必定执行useEffect函数. 现在, 我们第二个参数传递一个空数组: [], 即为不监听任何变化, 每次state/props改变不重新执行第一个参数的函数, 刷新浏览器, useEffect内函数的代码只执行了一次

useEffect(() => {
  setLoading(true);
  console.log("componentDidMount ?");
  setTimeout(() => {
    setLoading(false);
    setScheduleList(newScheduleList);
  }, 1000);
}, []);

我们至此还原了componentDidMount


componentDidUpdate

通常在class组件中, 我们对props传递过来的id不同而做一次网络请求,,如下:

componentDidUpdate(prevProps) {
  if (prevProps.selectedId !== props.selectedId) {
    // ... send xhr
    this.fetchData();
  }
}

为了实现这个业务, 我们仅需在useEffect方法第二个参数传递指定的监听值即可, 不需要再次调用一个useEffect, 否则会在第一次渲染时候触发2次fetchData

const fetchData = () => {
  setLoading(true);
  console.log("triggered fetchData");
  setTimeout(() => {
    setLoading(false);
    setScheduleList(newScheduleList);
  }, 1000);
};
 
useEffect(() => {
  console.log("fetchData when clickTime diff");
  fetchData();
}, [clickTime]);

这样, 每次点击按钮就触发了重新请求


componentWillUnmount

通常我们在componentWillUnmount里移除事件监听, 取消Obvervable订阅, 以便及时清理内存 useEffect接受函数可以return一个函数, 作为componentWillUnmount的实现: Schedule组件修改代码:

useEffect(() => {
  console.log("fetchData when clickTime diff");
  fetchData();
  return () => {
    console.log("componentWillUnmount ?");
  };
}, [clickTime]);

TabList组件代码:

const TabList = () => {
  const [clickTimeState, setClickTimeState] = useState(0);
 
  const [showSchedule, setShowSchedule] = useState(true);
 
  const changeClickTime = () => {
    setClickTimeState(clickTimeState + 1);
  };
 
  const changeShowSchedule = () => {
    setShowSchedule(false);
  };
 
  return (
    <React.Fragment>
      <div>ClickTime: {clickTimeState}</div>
      <div>
        <button onClick={changeClickTime}>Just clickTime + 1</button>
      </div>
      <div>
        <button onClick={changeShowSchedule}>Unmount Schedule</button>
      </div>
      {showSchedule ? <Schedule clickTime={clickTimeState} /> : ""}
    </React.Fragment>
  );
};

点击Unmout Schedule按钮, 控制台输出了Schedule组件的console代码 看起来好像是完美’还原’, 但是点击clickTime按钮, 发现了新的问题: 控制台依旧输出了componentWillUnmount?的代码

useEffect里定义的返回函数看似在每次unmount时候执行, 实际上当状态改变时, 依旧会执行此返回函数, 因为我们监听了clickTime的变化, 这样操作相当浪费, 不符合预期效果

为了避免浪费的内存, 我们再次调用一次useEffect方法, 第二个参数传递一个空数组, 表示不监听任何值变化, 此时, Schedule组件内有2个useEffect方法调用:

useEffect(() => {
  console.log("fetchData when clickTime diff");
  fetchData();
  // return () => {
  //  console.log('componentWillUnmount ?');
  // }
}, [clickTime]);
 
useEffect(() => {
  return () => {
    console.log("componentWillUnmount ?");
  };
}, []);

此时, 刷新浏览器, 我们多次点击clickTime按钮, componentWillUnmount的log没有执行, 点击Unmount Schedule按钮, 代码才执行, 符合预期效果


shouldComponentUpdate

在class组件中, 我们通常在状态变更时候定义此函数来进行判断新旧状态, 避免不必要的重新渲染:

shouldComponentUpdate(nextProps, nextState) {
	return nextProps.selectedId !== props.selectedId || nextState.isLoading !== this.state.isLoading;
}

此处就不在useEffect的可控功能内了, 在React16.6中, 有一个memo方法, 该方法可记录储存当前组件, 仅当需要的props更新才重新渲染, 我们还可在调用memo的时候使用第二个参数, 传递一个函数, 函数的参数为prevProps和nextProps. 为了实现效果, 我们在TabList组件传递一个无用的参数uselessProp给Schedule组件: TabList组件代码:

const TabList = () => {
  const [clickTimeState, setClickTimeState] = useState(0);
 
  const [showSchedule, setShowSchedule] = useState(true);
 
  const [color, setColor] = useState("#1890ff");
 
  // 定义一个无用的prop传递给Schedule组件
  const [uselessProp, setUselessProp] = useState(0);
 
  const changeClickTime = () => {
    setClickTimeState(clickTimeState + 1);
  };
 
  const changeShowSchedule = () => {
    setShowSchedule(false);
  };
 
  const changeColor = () => {
    setColor("cyan");
  };
 
  const changeUselessProp = () => {
    setUselessProp(uselessProp + 1);
  };
 
  return (
    <React.Fragment>
      <div>ClickTime: {clickTimeState}</div>
      <div>
        <button onClick={changeClickTime}>Just clickTime + 1</button>
      </div>
      <div>
        <button onClick={changeShowSchedule}>Unmount Schedule</button>
        <button onClick={changeColor}>set Color to cyan</button>
        <button onClick={changeUselessProp}>set uselessProp change</button>
      </div>
      {showSchedule ? (
        <Schedule
          clickTime={clickTimeState}
          color={color}
          uselessProp={uselessProp}
        />
      ) : (
        ""
      )}
    </React.Fragment>
  );
};

Schedule组件:

const Schedule = ({ clickTime, color }) => {
  const [loading, setLoading] = useState(false);
  const [scheduleList, setScheduleList] = useState([]);
 
  const fetchData = () => {
    setLoading(true);
    console.log("triggered fetchData");
    setTimeout(() => {
      setLoading(false);
      setScheduleList(newScheduleList);
    }, 1000);
  };
 
  useEffect(() => {
    console.log("fetchData when clickTime diff");
    fetchData();
  }, [clickTime]);
 
  useEffect(() => {
    return () => {
      console.log("componentWillUnmount ?");
    };
  }, []);
 
  // 查看渲染次数
  // 不使用memo, 每次改变uselessProp都会重新渲染
  console.log("rendering schedule...");
 
  return (
    <React.Fragment>
      {loading ? (
        "loading..."
      ) : (
        <ul>
          {scheduleList.map((item) => (
            <li style={{ color: color }} key={item.id}>
              {item.id} -- {item.action}
            </li>
          ))}
        </ul>
      )}
    </React.Fragment>
  );
};
 
export default Schedule;

使用memo包装Schedule组件, 仅需修改导出代码(注意: 函数返回值与shouleComponentUpdate相反)

// export default Schedule;
export default React.memo(Schedule, (prevProps, nextProps) => {
  // 注意: 此处的函数返回值和shouldComponentUpdate截然相反, 返回true是不重新渲染, 返回false是重新渲染
  return (
    prevProps.clickTime === nextProps.clickTime &&
    prevProps.color === nextProps.color
  );
});

React hooks的可共享逻辑

自定义hooks

定义一个文件在src/hooks下,命名为http.js, 复制Schedule组件的fetchData代码:

mport { useState } from 'react';
 
const fetchData = (url, data) => {
  return new Promise((res, rej) => {
    setTimeout(() => {
      if (url !== '/') {
        res({
          success: true,
          data: data
        });
      } else {
        rej({
          success: false,
          message: 'not valid url'
        });
      }
    }, 1000);
  });
};
 
export const useHttp = (url, data) => {
  const [loading, setLoading] = useState(false);
  const [fetchedData, setFetchedData] = useState(null);
 
  console.log('triggered fetchData');
  setLoading(true);
  fetchData(url, data)
    .then((res) => {
      setFetchedData(res);
    })
    .catch((err) => {
      setFetchedData(err);
    })
    .finally(() => {
      setLoading(false);
    });
  return [loading, fetchedData];
};

但是这个hooks是不合理的, 往往组件内逻辑是很复杂的, 我们可能要执行一次或监听不同的值而执行多次useHttp, 这样就要写不止一次的useHttp() 我们应该在useHttp方法内引用useEffect使得该自定义钩子更加灵活, 可以不必在useEffect内调用useHttp

import { useState, useEffect } from "react";
 
// mock Data
const actions = ["eat", "sleep"];
 
let pointer = 0;
 
const newScheduleList = [];
for (let i = 0; i < 10; i++) {
  if (actions[pointer]) {
    newScheduleList.push({
      id: i,
      action: actions[pointer],
    });
  } else {
    newScheduleList.push({
      id: i,
      action: actions[0],
    });
    pointer = 0;
  }
  pointer++;
}
// end mock Data
 
const fetchData = (url, data) => {
  console.log("fetchData params:", url, data);
  return new Promise((res, rej) => {
    setTimeout(() => {
      if (url === "/scheduleList") {
        res({
          success: true,
          data: newScheduleList,
        });
      } else {
        rej({
          success: false,
          message: "not valid url",
        });
      }
    }, 1000);
  });
};
 
export const useHttp = (url, data, dependencies) => {
  const [loading, setLoading] = useState(false);
  const [fetchedData, setFetchedData] = useState(null);
 
  console.log("triggered fetchData");
  useEffect(() => {
    setLoading(true);
    fetchData(url, data)
      .then((res) => {
        setFetchedData(res);
      })
      .catch((err) => {
        setFetchedData(err);
      })
      .finally(() => {
        setLoading(false);
      });
  }, dependencies);
 
  return [loading, fetchedData];
};

修改Schedule组件的逻辑

import React, { useEffect } from "react";
import { useHttp } from "../hooks/http";
 
const Schedule = ({ clickTime, color }) => {
  // 不必再去定义loading状态, loading现在由useHttp控制
  // const [loading, setLoading] = useState(false);
  // const [scheduleList, setScheduleList] = useState([]);
 
  // clickTime为我们监听的值
  const [loading, fetchedData] = useHttp("/scheduleList", { test: 1 }, [
    clickTime,
  ]);
  // 我们定义的fetchedData初始值为null, 一定要做判断
  const scheduleList = fetchedData
    ? fetchedData.data.map((item) => ({
        ...item,
        success: true,
      }))
    : [];
 
  // useEffect(() => {
  //   console.log('fetchData when clickTime diff');
  //
  // }, [clickTime]);
 
  useEffect(() => {
    return () => {
      console.log("componentWillUnmount ?");
    };
  }, []);
 
  console.log("rendering schedule...");
 
  return (
    <React.Fragment>
      {loading ? (
        "loading..."
      ) : (
        <ul>
          {scheduleList.map((item) => (
            <li style={{ color: color }} key={item.id}>
              {item.id} -- {item.action}
            </li>
          ))}
        </ul>
      )}
    </React.Fragment>
  );
};
 
export default React.memo(Schedule, (prevProps, nextProps) => {
  return (
    prevProps.clickTime === nextProps.clickTime &&
    prevProps.color === nextProps.color
  );
});

刷新浏览器, 每次clickTime只会触发一次http请求, 完美抽离了http hooks


总结

useState: 

const [state, setState] = useState({});
// 替换 state + setState

useEffect :

// 替换componentDidMount
useEffect(() => {
  // code here...
}, []);
 
// 替换componentWillUnmount
useEffect(() => {
  return () => {
    // code here...
  };
}, []);
 
// 部分替换componentDidUpdate处理逻辑
useEffect(() => {
  // 当value1, value2变化, 执行下列代码
  // code here....
}, [value1, value2]);
 
// 替换componentDidUpdate(不建议以下写法, 每次更新都会触发)
useEffect(() => {
  // code here...
});

 memo:

// 替换shouldComponentUpdate
React.memo(ReactComponent); // 自行判断是否更新
React.memo(ReactComponent, (prevProps, nextProps) => {
  // 当id相同, 不重新渲染, 此处返回值与shouldComponentUpdate相反
  return prevProps.id === nextProps.id;
});

注意: 使用useEffect时不能嵌套在代码块中使用, React仅会在组件内寻径一层, 譬如, 以下代码不会生效:

if (id === 0) {
  useEffect(() => {}, []); // wrong way!
}