React Hooks 中的 Context 如何避免重新渲染?

27

我在我的React应用中使用带有hooks的React context作为状态管理器。每次存储中的值发生更改时,所有组件都会重新渲染。

有没有办法防止React组件重新渲染?

存储配置:

import React, { useReducer } from "react";
import rootReducer from "./reducers/rootReducer";

export const ApiContext = React.createContext();

export const Provider = ({ children }) => {
  const [state, dispatch] = useReducer(rootReducer, {});

  return (
    <ApiContext.Provider value={{ ...state, dispatch }}>
      {children}
    </ApiContext.Provider>
  );
};

一个 reducer 示例:

import * as types from "./../actionTypes";

const initialState = {
  fetchedBooks: null
};

const bookReducer = (state = initialState, action) => {
  switch (action.type) {
    case types.GET_BOOKS:
      return { ...state, fetchedBooks: action.payload };

    default:
      return state;
  }
};

export default bookReducer;

根Reducer,可以组合尽可能多的Reducer:

import userReducer from "./userReducer";
import bookReducer from "./bookReducer";

const rootReducer = ({ users, books }, action) => ({
  users: userReducer(users, action),
  books: bookReducer(books, action)
});

一个行动的例子:

import * as types from "../actionTypes";

export const getBooks = async dispatch => {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos/1", {
    method: "GET"
  });

  const payload = await response.json();

  dispatch({
    type: types.GET_BOOKS,
    payload
  });
};
export default rootReducer;

这是书籍组件:

import React, { useContext, useEffect } from "react";
import { ApiContext } from "../../store/StoreProvider";
import { getBooks } from "../../store/actions/bookActions";

const Books = () => {
  const { dispatch, books } = useContext(ApiContext);
  const contextValue = useContext(ApiContext);

  useEffect(() => {
    setTimeout(() => {
      getBooks(dispatch);
    }, 1000);
  }, [dispatch]);

  console.log(contextValue);

  return (
    <ApiContext.Consumer>
      {value =>
        value.books ? (
          <div>
            {value.books &&
              value.books.fetchedBooks &&
              value.books.fetchedBooks.title}
          </div>
        ) : (
          <div>Loading...</div>
        )
      }
    </ApiContext.Consumer>
  );
};

export default Books;

当 Books 组件中的值发生变化时,另一个名为 Users 的组件将重新渲染:

import React, { useContext, useEffect } from "react";
import { ApiContext } from "../../store/StoreProvider";
import { getUsers } from "../../store/actions/userActions";

const Users = () => {
  const { dispatch, users } = useContext(ApiContext);
  const contextValue = useContext(ApiContext);

  useEffect(() => {
    getUsers(true, dispatch);
  }, [dispatch]);

  console.log(contextValue, "Value from store");

  return <div>Users</div>;
};

export default Users;

如何在上下文重新渲染时进行最佳优化?提前致谢!


你有一个演示这个的 CodeSandbox 吗? - Matt Oestreich
看起来你用hooks+context自己创建了你自己的Redux :) - Kutyel
你是怎么知道其他组件在重新渲染的?有什么方法可以判断吗?看起来发生的事情很正常——每次更改路由时,导航链接都会重新渲染。这是你所指的吗? - Matt Oestreich
4个回答

25

BooksUsers当前在每个循环中都会重新渲染 - 不仅在存储值更改的情况下。

1. 属性和状态更改

React从发生属性或状态变化的组件作为根开始,重新渲染整个子组件树。您通过getUsers更改父级状态,因此BooksUsers会重新渲染。

const App = () => {
  const [state, dispatch] = React.useReducer(
    state => ({
      count: state.count + 1
    }),
    { count: 0 }
  );

  return (
    <div>
      <Child />
      <button onClick={dispatch}>Increment</button>
      <p>
        Click the button! Child will be re-rendered on every state change, while
        not receiving any props (see console.log).
      </p>
    </div>
  );
}

const Child = () => {
  console.log("render Child");
  return "Hello Child ";
};


ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>

优化技巧

使用React.memo防止组件重新渲染,如果它自己的props实际上没有改变。

// prevents Child re-render, when the button in above snippet is clicked
const Child = React.memo(() => {
  return "Hello Child ";
});
// equivalent to `PureComponent` or custom `shouldComponentUpdate` of class comps

重要提示:React.memo仅检查属性更改(useContext值更改会触发重新渲染)!


2. 上下文变化

所有上下文消费者(useContext)在上下文值发生变化时会自动重新渲染。

// here object reference is always a new object literal = re-render every cycle
<ApiContext.Provider value={{ ...state, dispatch }}>
  {children}
</ApiContext.Provider>

优化技巧

确保上下文值具有稳定的对象引用,例如通过使用useMemo Hook。

const [state, dispatch] = useReducer(rootReducer, {});
const store = React.useMemo(() => ({ state, dispatch }), [state])

<ApiContext.Provider value={store}>
  {children}
</ApiContext.Provider>

其他

不确定为什么你要在Books中将所有这些结构组合在一起,只需使用一个useContext

const { dispatch, books } = useContext(ApiContext);
// drop these
const contextValue = useContext(ApiContext); 
<ApiContext.Consumer> /* ... */ </ApiContext.Consumer>; 

您还可以查看此代码示例,该示例同时使用React.memouseContext


3
我认为这里发生的是预期行为。它渲染两次的原因是因为你在访问书籍或用户页面时自动获取了新的书籍/用户。
这是因为页面加载后,useEffect启动并获取一本书或一个用户,然后页面需要重新渲染才能将新获取的书籍或用户放入DOM。
我修改了您的CodePen以表明这一点。如果您在书籍或用户页面上禁用“自动加载”(我添加了一个按钮),然后浏览该页面,再返回到该页面,您将看到它只渲染一次。
我还添加了一个按钮,允许您随时获取新的书籍或用户...这是为了展示只有您所在的页面会重新渲染。
总的来说,据我所知,这是预期的行为。

Edit react-state-manager-hooks-context


2
我尝试用不同的例子来解释,希望能有所帮助。
由于Context使用引用标识来确定何时重新渲染,这可能会在提供程序的父级重新渲染时触发消费者的意外重新渲染。
例如:下面的代码每次Provider重新渲染时都会重新渲染所有的消费者,因为value总是创建一个新对象。
class App extends React.Component {
  render() {
   return (
      <Provider value={{something: 'something'}}>
        <Toolbar />
      </Provider>
    );
 }
}

为了解决这个问题,将该值提升到父组件的状态中。
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},
    };
  }

  render() {
    return (
      <Provider value={this.state.value}>
        <Toolbar />
      </Provider>
    );
  }
}

所以,我需要创建一个子组件来显示书籍。例如,在Books组件中从后端获取数据并将其传递给BooksItem,这样可以防止重新渲染,对吧? - lecham
3
既然父组件状态发生了改变,子组件难道不会自动重新渲染吗? - iqbal125
你值得获得奖励, 在我的情况下,我使用了useMemo来创建值对象。 - undefined

0

在React中,防止组件渲染的解决方案称为shouldComponentUpdate。它是React类组件上可用的生命周期方法。与之前作为功能无状态组件的Square不同:

const Square = ({ number }) => <Item>{number * number}</Item>;

您可以使用一个带有componentShouldUpdate方法的类组件:
class Square extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    ...
  }

  render() {
    return <Item>{this.props.number * this.props.number}</Item>;
  }
}

正如您所看到的,shouldComponentUpdate 类方法在运行组件重新渲染之前可以访问下一个 props 和 state。这就是您可以通过从此方法返回 false 来决定阻止重新渲染的地方。如果您返回 true,则组件将重新渲染。

class Square extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.number === nextProps.number) {
      return false;
    } else {
      return true;
    }
  }

  render() {
    return <Item>{this.props.number * this.props.number}</Item>;
  }
}

在这种情况下,如果传入的数字属性没有改变,组件就不应该更新。通过向组件添加控制台日志来自己尝试一下。当透视图更改时,Square 组件不应重新渲染。这对于您的 React 应用程序是一个巨大的性能提升,因为所有子组件不会在其父组件的每次重新渲染时重新渲染。最后,防止组件重新渲染取决于您自己。
理解这个 componentShouldUpdate 方法肯定会帮助您!

1
谢谢!但我使用钩子,所以我会尝试使用memo。 - lecham

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接