React组件通信的几种方式--TypeScript

React组件通信的几种方式--TypeScript

通过todoList示例进行学习,首先创建基于TypeScript的react工程:
npx create-react-app myapp --template typescript
示例:

image.png

一、props&Event

  • 父组件向子组件通信:
    父组件通过向子组件传递 props,子组件得到 props 后进行相应的处理。
  • 子组件向父组件通信:
    利用回调函数,父组件将一个函数作为 props 传递给子组件,子组件调用该回调函数,便可以向父组件通信。
  • 跨级组件通信:
    跨级组件通信,就是父组件向子组件的子组件通信,向更深层的子组件通信,解决方法就是中间组件层层传递 props。
    如果父组件嵌套较深,那么中间的每一层组件都要去传递 props,增加了复杂度,也给后期维护带来不便,并且这些 props 并不是这些中间组件自己所需要的。

父组件传递props:

// 父组件
const Todo = () => {
    // 把数据统一存到todo父组件里
    const [todoList, setTodoList] = useState<StateProps[]>([]);
    // 改变todo
    const changeTodo = (id: number) => {
        const newTodoList = todoList.map(item => {
            if (item.id === id) {
                return Object.assign({}, item, { isFinished: !item.isFinished })
            }
            return item;
        })
        setTodoList(newTodoList);
    }
    // 添加todo
    const addTodo = (todo: StateProps) => {
        setTodoList([...todoList, todo]);
    }
    return (
        <div className="todo">
            <TodoInput addTodo={addTodo} />
            <TodoList todoList={todoList} changeTodo={changeTodo} />
        </div>
    )
}

子组件接收 props 与调用回到函数:

// 子组件
const TodoInput = ({ addTodo }: IProps) => {
    const [text, setText] = useState('');
    const handleChangeText = (e: React.ChangeEvent) => {
        setText((e.target as HTMLInputElement).value);
    }
    const handleAddTodo = () => {
        if (!text) return;
        addTodo({
            id: new Date().getTime(),
            text: text,
            isFinished: false,
        })
        setText('');
    }
    return (
        <div className="todo-input">
            <input type="text" placeholder="请输入代办事项" onChange={handleChangeText} value={text} />
            <button style={{ marginLeft: '10px' }} onClick={handleAddTodo} >+添加</button>
        </div>
    )
}

const TodoList = ({ todoList, changeTodo }: IProps) => {
    return (
        <div className="todo-list" style={style}>
            {todoList.map(item => <TodoItem key={item.id} todo={item} changeTodo={changeTodo} />)}
        </div>
    )
}

// 孙子组件
const TodoItem = ({ todo, changeTodo }: IProps) => {
    // 改变事项状态
    const handleChange = () => {
        changeTodo(todo.id);
    }
    return (
        <div className="todo-item" style={style}>
            <input type="checkbox" checked={todo.isFinished} onChange={handleChange} />
            <span style={{ textDecoration: todo.isFinished ? 'line-through' : 'none' }}>{todo.text}</span>
        </div>
    )
}

二、Context

context 相当于一个全局变量,是一个大容器,我们可以把要通信的内容放在这个容器中,这样一来,不管嵌套有多深,都可以随意取用。
使用 context 也很简单,需要满足两个条件:

  • 上级组件要声明自己支持 context,并提供一个函数来返回相应的 context 对象;
  • 子组件要声明自己需要使用 context。
    使用步骤:
    (1)通过 createContext 创建一个 context 对象,然后给这个对象指定属性值;
    (2)通过用 <MyContext.Provider> </MyContext.Provider> 组件包裹需要接收上层属性的子组件;
    (3)将需要传递的属性放在 Provider 组件的 value 属性上向下传递;
    (4)在子组件中获取值时使用 useContext 来简化操作。
// Provide组件
import React, { createContext, useState } from "react";
export interface StateProps {
    id: number;
    text: string;
    isFinished: boolean;
}
export interface ContextProps {
    todoList: StateProps[];
    changeTodo: (id: number) => void;
    addTodo: (todo: StateProps) => void;
}
// const MyContext = createContext<ContextProps | null>(null); // 泛型写法
export const MyContext = createContext({} as ContextProps); // 断言写法
const MyProvide = (props: React.PropsWithChildren<{}>) => {
    // 把数据统一存到todo父组件里
    const [todoList, setTodoList] = useState<StateProps[]>([]);
    // 改变todo
    const changeTodo = (id: number) => {
        const newTodoList = todoList.map(item => {
            if (item.id === id) {
                return Object.assign({}, item, { isFinished: !item.isFinished })
            }
            return item;
        })
        setTodoList(newTodoList);
    }
    // 添加todo
    const addTodo = (todo: StateProps) => {
        setTodoList([...todoList, todo]);
    }
    return (
        <MyContext.Provider value={{ todoList, changeTodo, addTodo }} >
            {/* 插槽内容 */}
            {props.children}
        </MyContext.Provider>
    )
}
export default MyProvide;

// 子组件
import React, { useContext } from 'react';
import TodoItem from './TodoItem';
import { MyContext } from './MyProvider';
const style = {
    marginTop: '20px',
}
const TodoList = () => {
    const { todoList } = useContext(MyContext);
    return (
        <div className="todo-list" style={style}>
            {todoList.map(item => <TodoItem key={item.id} todo={item} />)}
        </div>
    )
}

以上方法中的todoList是一个相对复杂的数据,使用useState定义不太好维护,所以还可以使用 useReducer 进行优化。

// 将业务逻辑拆分到一个单独文件中,方便进行状态管理
export interface StateProps {
    id: number;
    text: string;
    isFinished: boolean;
}
export interface ActionProps {
    type: string;
    [key: string]: any;
}
const reducer = (state: StateProps[], action: ActionProps) => {
    switch (action.type) {
        case 'ADD':
            return [...state, action.todo];
        case 'CHANGESTATUS':
            return state.map(item => {
                if (item.id === action.id) {
                    return Object.assign({}, item, { isFinished: !item.isFinished })
                }
                return item;
            });
        default:
            return state;
    }
}
export default reducer;

// Provider 组件
import React, { createContext, useReducer } from "react";
import reducer, { StateProps, ActionProps } from "../store/reducer";
export interface ContextProps {
    state: StateProps[];
    dispatch: React.Dispatch<ActionProps>;
}
// const MyContext = createContext<ContextProps | null>(null); // 泛型写法
export const MyContext = createContext({} as ContextProps); // 断言写法
const MyProvide = (props: React.PropsWithChildren<{}>) => {
    // 把数据统一存到todo父组件里
    // const [todoList, setTodoList] = useState<StateProps[]>([]);
    // 改用useReducer进行优化
    const initState: StateProps[] = [];
    const [state, dispatch] = useReducer(reducer, initState)
    return (
        <MyContext.Provider value={{ state, dispatch }} >
            {/* 插槽内容 */}
            {props.children}
        </MyContext.Provider>
    )
}

需要注意 dispatch 中的 type 值需要与 reducer 中定义的一一对应。

三、Redux

先安装相关依赖:
npm i -S redux react-redux @types/react-redux

Redux基本概念
  • Store (状态容器)
    Store 就是保存数据的地方,整个应用只能有一个 Store。
  • State
    Store对象包含所有数据。当前时刻的 State,可以通过store.getState()拿到。Redux 规定, 一个 State 对应一个 View。
  • reducer
    是一个纯函数(什么样的输入就有什么样的输出),它接受 Action 和当前 State 作为参数,返回一个新的 State,不会进行 DOM 操作和 Ajax 请求,不能调用Date.now()或者Math.random()等不纯的方法,因为每次会得到不一样的结果。
  • action
    Action 就是 View 发出的通知,表示 State 应该要发生变化了。
    Action 是一个对象。其中的type属性是必须的,表示 Action 的名称,其他属性可以自由设置。
  • dispatch
    dispatch 是 View 发出 Action 的唯一方法。
使用步骤:

1、通过 createStore 实例化 store 实例:

import { createStore } from "redux";
import reducer from "./reducer";
const store = createStore(reducer, []);

2、在根组件引入 Provider 组件,将其作为最外侧,并传入 store 实例,这样所有子组件都能拿到 store 里的状态:

import { Provider } from 'react-redux';
import store from '../store';
// 父组件
const Todo = () => {
    return (
        <Provider store={store}>
            <div className="todo">
                <TodoInput />
                <TodoList />
            </div>
        </Provider>
    )
}

3、在 reducer 函数中定义状态和状态处理,其实就是根据不同的 action 对象分别对状态做不同的处理:

const reducer = (state: StateProps[] = [], action: ActionProps) => {
    switch (action.type) {
        case types.ADD:
            return [...state, action.todo];
        case types.CHANGESTATUS:
            return state.map(item => {
                if (item.id === action.id) {
                    return Object.assign({}, item, { isFinished: !item.isFinished })
                }
                return item;
            });
        default:
            return state;
    }
}

4、定义 action 对象:

import * as types from './actionType';
import { StateProps } from './reducer';
export const addAction = (todo: StateProps) => ({
    type: types.ADD,
    todo,
})
export const changeAction = (id: number) => ({
    type: types.CHANGESTATUS,
    id,
})

5、当在某一个事件发生的时候,通过调用 dispatch 传入 action 对象,从而实现 state 的改变,这里引入 hooks 的 useDispatch 方法简化代码:

import { useDispatch } from 'react-redux'; // 减少代码复杂度
import { addAction } from '../store/action';

const dispatch = useDispatch();

dispatch(addAction({
            id: new Date().getTime(),
            text: text,
            isFinished: false,
        }))

6、使用 useSelector 获取状态值:

import { useSelector } from 'react-redux';
import { RootState } from '../store/reducer';

const state = useSelector((state: RootState) => state);  // 返回具体要拿的store中的state或者整个store

四、Mobx

先安装相关依赖:
npm i mobx mobx-react

使用步骤

1、定义一个状态类,在类中定义状态以及修改状态的方法,并调用 mobx 中的 makeAutoObservable 方法将状态和方法转换为可观察的:

import { makeAutoObservable } from 'mobx';

class TodoStore {
    todoList: StateProps[] = [];
    constructor() {
        makeAutoObservable(this); // 自动转换
    }
    // 添加事项
    addAction(todo: StateProps) {
        this.todoList.push(todo)
    }
    // 改变事项状态
    changeAction(id: number) {
        this.todoList = this.todoList.map(item => {
            if (item.id === id) {
                return Object.assign({}, item, {
                    isFinished: !item.isFinished
                })
            }
            return item;
        })
    }
}

2、配合 react 的 createContext 将数据注入子组件:

import React, { createContext, useContext } from 'react';
import todoStore from './TodoStore';
export const MyContext = createContext<typeof todoStore>(todoStore);

const MyProvider: React.FC = ({ children }) => {
    return (
        <MyContext.Provider value={todoStore}>
            {children}
        </MyContext.Provider>
    )
}

3、自定义一个 hooks 用来获取 store 数据:

// 自定义 hook 获取 store
export const useStore = () => {
    const store = useContext(MyContext);
    if (!store) throw Error('no store');
    return store;
}

4、通过 mobx 的 observer 包裹需要引用 store 数据的组件,将其变为响应式组件:

import React from 'react';
import { observer } from 'mobx-react';
import TodoItem from './TodoItem';
import { useStore } from '../store/index';
// 子组件
const TodoList = () => {
    const store = useStore();
    return (
        <div className="todo-list">
            {store.todoList.map(item => <TodoItem key={item.id} todo={item} />)}
        </div>
    )
}
export default observer(TodoList);

用 useRef 和 useEffct 对 todoList 做优化:

import React, { useRef, useEffect } from 'react';
import { useStore } from '../store/index';
import { observer } from 'mobx-react';
const TodoInput = () => {
    // const [text, setText] = useState('');
    const inputRef = useRef<HTMLInputElement>(null);
    const store = useStore();
    // const handleChangeText = (e: React.ChangeEvent) => {
    //     setText((e.target as HTMLInputElement).value);
    // }
    // 相当于 componentDidMount、componentDidUpdate、componentWillUnmount 的集合
    useEffect(() => {
        inputRef?.current!.focus();
    }, [])
    const handleAddTodo = () => {
        const value = inputRef.current!.value;
        store.addAction({
            id: new Date().getTime(),
            text: value,
            isFinished: false,
        })
        inputRef.current!.value = '';
    }
    return (
        <div className="todo-input">
            {/* <input type="text" placeholder="请输入代办事项" onChange={handleChangeText} value={text} /> */}
            <input type="text" placeholder="请输入代办事项" ref={inputRef} />
            <button style={{ marginLeft: '10px' }} onClick={handleAddTodo} >+添加</button>
        </div>
    )
}
export default observer(TodoInput);

五、总结

1、props&Event 针对组件嵌套层级比较浅;
2、Context 结合 Hooks 也是比较简单的,适合解决一些小型数据管理;
3、Mobx 和 Redux 状态管理库适合管理大型数据。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容

  • 首次发表在个人博客 需要组件之进行通信的几种情况 父组件向子组件通信 子组件向父组件通信 跨级组件通信 没有嵌套关...
    IOneStar阅读 1,417评论 0 10
  • 在使用 React 的过程中,不可避免的需要组件间进行消息传递(通信),组件间通信大体有下面几种情况: 父组件向子...
    柏丘君阅读 93,090评论 7 80
  • 今天来看一下react组件之间是怎么进行通讯的。react推崇的是单向数据流,自上而下进行数据的传递,但是由下而上...
    亲亲qin阅读 5,984评论 2 12
  • 生命周期与钩子函数(重点) 生命周期指的react实例及组件从创建到运行到销毁的完整的过程。 组件的生命周期可分成...
    未来在奋斗阅读 133评论 0 1
  • 需要组件之进行通信的几种情况 父组件向子组件通信 子组件向父组件通信 跨级组件通信 没有嵌套关系组件之间的通信 1...
    半夜成仙阅读 979评论 1 3