React Context 的使用

  使用React开发应用程序时,如果多个组件需要共享数据,可以把数据放到父组件中,通过属性向下传递给子组件。但当组件嵌套较深时,两个最底层的组件要想共享数据,那就霜要把数据放到最顶层的组件中,然后再一层一层向下传递,可能需要向下传递好多层才能到达想要数据的子组件,这就产生了一个问题,由于经过的这些层(组件)可能不需要这个数据,在向下传递的过程中,有可能就忘记写共享属性,程序也就出错了,并且还不好调试。有没有一种方法,可以穿透组件,想要数据的子组件直接获取到最顶层组件的共享数据, 这就是React context。

  Context就是上下文,环境的意思,React 的context 就是提供了一个上下文或环境,在这个环境中,有context提供者和context消费者,提供者(provider) 提供共享数据,消费者(consumer) 消费这些共享数据。怎么提供和消费呢?provider和consumer是两个组件,provider有一个value属性, 只要把共享的数据放到value中,然后再把consumer组件包起来,consumer组件就能获到到共享数据。consumer组件消费数据的方式有两种,传统的是组件内部是回调函数,现代一点的是useContext hooks。

  create-react-app react-context 创建项目 ,cd react-context && npm install bootstrap。j假设Header中有一个user下拉框,选择user,然后Body中显示user,胡乱编造的一个例子。

   App.js

import React from "react";
import 'bootstrap/dist/css/bootstrap.css';  // 添加bootstrap 样式

import Header from "./Header";
import Details from './Details';

export default function App() {
    return (
        <div className="card">
             <div className="card-header">
                <Header></Header>
            </div>
            <div className="card-body">
                <Details></Details>
            </div>
        </div>
    )
}

  Header.js  

import React from "react";
import UserPicker from "./UserPicker";

export default function Header() {
    return (
        <div className="container">
            <div className="row">
                <div className="col-sm">
                    <button type="button" className="btn btn btn-light">Booking</button>
                    <button type="button" className="btn btn btn-light">BookList</button>
                </div>
                <div className="col-sm">
                    <UserPicker></UserPicker>
                </div>
            </div>
        </div>
    )
}

  UserPicker.js

const users = ['', 'sam', 'jason'];

export default function UserPicker() {

    // onChange={handleSelect}
    return (
        <select value={'sam'}>
            {users.map(name => (
                <option key={name} value={name}>{name}</option>
            ))}
        </select>
    );
}v

  Detials.js

const booking = [
    {
      "name": 'sam',
      "title": "Meeting Room",
    },
    {
      "name": 'json',
      "title": "Lecture Hall",
    }
]


export default function Details() {

  return (
    <div className="booking-details" style={{marginLeft: '120px'}}>
      <h5 className="card-title"> Booking Details </h5>
      <p className="card-text">{booking[0].name} {booking[0].title}</p>
    </div>
  );
}

  UserPicker和Details共享user数据,且在UserPicker中,可以选择user。因此在最外层的组件App中,要设一个user状态和改变user的setUser,让这两个数据共享。使用React.createContext()创建一个context,它接受一个可选的参数,就是共享数据的默认值,当然可以不传,直接调用createContext() 方法。不过,建议写上参数,把它看作是共享数据的格式定义,一看到这个context, 就知道要共享什么。那创建的context 放在什么地方呢?context 可以定义在任意位置,在src 目录下新建一个文件UserContext.js 来存放context.  

import React from 'react';

export const UserContext = React.createContext({
    user: '',
    setUser: () => {}
})

  使用provider(<UserContext.Provider />组件)来提供共享数据,在App.js中,使用userState,然后把user和setUser放到Provider的value属性中,最后使用Provider把UserPicker和Details组件包起来

import { UserContext } from './UserContext';

export default function App() {
    const [user, setUser] = useState('');

    return (
        <UserContext.Provider value={{user, setUser}}>
            <div className="card">
                <div className="card-header">
                    <Header></Header>
                </div>
                <div className="card-body">
                    <Details></Details>
                </div>
            </div>
        </UserContext.Provider>
    )
}

  UserPicker使用共享数据,最开始的时候,是使用Consumer组件。Consumer 组件的内容是一个函数表达式,函数的参数,就是Consumer组件帮我们注入到组件中的共享数据,它这时就可以直接使用共享数据了。

 
 import { UserContext } from './UserContext';
export default function UserPicker() {
    return (
        <UserContext.Consumer>
            {(context) => {
                const {user, setUser} = context;
                return (
                    <select value={user} onChange={(e) => setUser(e.target.value)}>
                        {users.map(name => (
                            <option key={name} value={name}>{name}</option>
                        ))}
                    </select>
                )
            }}
        </UserContext.Consumer>
    );
}

  Details中使用共享数据

import { UserContext } from './UserContext';

export default function Details() {

  return (
    <UserContext.Consumer>
      { (context) => {
        const { user } = context;
        const seleted = booking.filter(data => data.name === user);

        return (
          <div className="booking-details" style={{ marginLeft: '120px' }}>
            <h5 className="card-title"> Booking Details </h5>
            <p className="card-text">
              {seleted.length > 0 &&
                <React.Fragment>
                  {seleted[0].name} {seleted[0].title}
                </React.Fragment>
              }
            </p>
          </div>
        )
      }}
    </UserContext.Consumer>
  );
}

  如果使用的是React 16.8以上,可以使用useContext hooks,UserPicker.js

import { useContext } from 'react';
import { UserContext } from './UserContext';

export default function UserPicker() {
    const {user, setUser} = useContext(UserContext);

    return (
        <select value={user} onChange={(e) => setUser(e.target.value)}>
            {users.map(name => (
                <option key={name} value={name}>{name}</option>
            ))}
        </select>
    );
}

  Details.js

import { useContext } from 'react';
import { UserContext } from './UserContext';

export default function Details() {

  const { user } = useContext(UserContext);
  const seleted = booking.filter(data => data.name === user);

  return (
    <div className="booking-details" style={{ marginLeft: '120px' }}>
      <h5 className="card-title"> Booking Details </h5>
      <p className="card-text">
        {seleted.length > 0 &&
          <React.Fragment>
            {seleted[0].name} {seleted[0].title}
          </React.Fragment>
        }
      </p>
    </div>
  )
}

  这也会带来一个问题,当user 改变的时候,App所有子组件都会重新渲染,能不能只渲染consumer组件?要使用children属性,把要获取数据的子组件作为children传递给Provider. 定制一个Provider组件,接受children作为参数。UserContext.js

import React from 'react';
import {createContext, useState} from "react";

export const UserContext = createContext({
    user: '',
    setUser: () => {}
})

export const UserProvider = ({children}) => {
    const [user, setUser] = useState('');

    return <UserContext.Provider value={{user, setUser}}>
        {children}
    </UserContext.Provider>
}

  App.js

import { UserProvider } from './UserContext';

export default function App() {

    return (
        <UserProvider>
            <div className="card">
                <div className="card-header">
                    <Header></Header>
                </div>
                <div className="card-body">
                    <Details></Details>
                </div>
            </div>
        </UserProvider>
    )
}

  当在userPicker中调用setUser改变user时,UserProvider组件会重新渲染,但是UserProvider的children不会重新渲染,因为children并没有改变,只有消费context的组件(children)才会重新渲染。UserProvider是从props中获取children, 更新组件内部的state并不会改变props。当后代组件中调用setUser, children的identity并不会改变。它和状态改变以前是同一个object,所以没有必要重新渲染children. 但context的consumer 并不一样,当Provider的值改变时,consumer 会重新渲染。Any components that consume the context, however, do re-render in response to the change of value on the provider, not because the whole tree of components has re-rendered.

  但使用object作为context的值,好不好?是不是把所有的共享数据都放到一个对象中?

value = {
    theme: "lava",
    user: 'sam',
    language: "en"
};
<Context.Provider value={value}><App/></Context.Provider>

  在子组件中,有的组件消费theme, 有的组件消费user, 有的组件消费language。如果value中有一个值改变,所有consuer都会重新渲染,没有必要。可以使用多个provider

<ThemeContext.Provider value="lava">
    <UserContext.Provider value="sam">
        <LanguageContext.Provider value="en">
            <App />
        </LanguageContext.Provider>
    </UserContext.Provider>
</ThemeContext.Provider>

  子组件,可以只想获取自己想要的值。

function InfoPage(props) {
    const theme = useContext(ThemeContext);
    const language = useContext(LanguageContext);
    return (/* UI */);
}

function Messages(props) {
    const theme = useContext(ThemeContext);
    const user = useContext(UserContext);
    // subscribe to messages for user
    return (/* UI */);
}

  定制一个AppProvider

function AppProvider({ children }) {
    // maybe manage some state here
    return (
        <ThemeContext.Provider value="lava">
            <UserContext.Provider value="sam">
            <LanguageContext.Provider value="en">
                {children}
            </LanguageContext.Provider>
    </UserContext.Provider>
    </ThemeContext.Provider >
    );
}

  使用它

<AppProvider>
    <App/>
</AppProvider>

   如果再仔细一点,user和setUser可以作两个provider

import { createContext, useState } from "react";

export const UserContext = createContext({
    user: ''
})
export const UserSetContext = createContext({
    setUser: () => { }
});

export function UserProvider({ children }) {
    const [user, setUser] = useState(null);

    return (
        <UserContext.Provider value={user}>
            <UserSetContext.Provider value={setUser}>
                {children}
            </UserSetContext.Provider>
        </UserContext.Provider>);
}

   有时,也不一定非要使用Context,React官网还提供了组件组合

export default function App({ user }) {
  const { username, avatarSrc } = user;

  return (
    <main>
      <Navbar username={username} avatarSrc={avatarSrc} />
    </main>
  );
}

function Navbar({ username, avatarSrc }) {
  return (
    <nav>
      <Avatar username={username} avatarSrc={avatarSrc} />
    </nav>
  );
}

function Avatar({ username, avatarSrc }) {
  return <img src={avatarSrc} alt={username} />;
}

  只有在App组件中知道Avatar组件的属性,可以直接创建一个Avatar组件向下传递

export default function App({ user }) {
  const { username, avatarSrc } = user;

  const avatar = <img src={avatarSrc} alt={username} />;

  return (
    <main>
      <Navbar avatar={avatar} />
    </main>
  );
}

function Navbar({ avatar }) {
  return <nav>{avatar}</nav>;
}

 

posted @ 2022-03-14 14:09  SamWeb  阅读(538)  评论(0编辑  收藏  举报