react组件通讯
一、父传子
javascript
import { useState, useRef } from "react";
const Son = (props) => {
return (
<div
style={{
height: "50%",
width: "50%",
background: "#fff",
margin: "50px",
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
}}
>
<span>this is son, message from {props.name}</span>
</div>
);
};
function App() {
const name = "father"
return (
<div>
<div style={{ height: "200px", width: "800px", background: "#eee" }}>
this is app, {msg}
<Son name={name}/>
</div>
</div>
);
}
export default App;
原理就是通过props传递自定义属性,与vue原理一样,但比vue更容易理解
二、子传父
javascript
import { useState, useRef } from "react";
const Son = (props) => {
const sonMsg = 'message from son'
return (
<div
style={{
height: "50%",
width: "50%",
background: "#fff",
margin: "50px",
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
}}
>
<span>this is son, message from {props.name}</span>
<button style={{width: '50%'}} onClick={() => props.onGetMsg(sonMsg)}>send</button>
</div>
);
};
function App() {
const name = "father";
const [msg, setMsg] = useState('self message')
const getMsg = (message) => {
setMsg(message)
}
return (
<div>
<div style={{ height: "200px", width: "800px", background: "#eee" }}>
this is app, {msg}
<Son name={name} onGetMsg={getMsg}/>
</div>
</div>
);
}
export default App;
点击send按钮前:
点击send后,红色文字变化
原理及步骤:
- 父组件中使用useState定义响应式状态数据msg及修改方法setMsg
- 父组件中定义修改状态数据的方法getMsg
- 父组件向子组件Son传递一个自定义方法(也可以理解为广义的属性)onGetMsg,然后把父组件中定义的getMsg作为参数传给子组件,这里说一句,子传父实际上也用到了父传子,父传给子一个方法
- 子组件中的按钮绑定点击事件,点击事件的回调函数就是父组件传过来的getMsg方法,只不过属性名是onGetMsg,并向这个方法中传递实参sonMsg,要注意,按钮的点击事件是默认带参的,如果要传递自己定义的参数,需要写成函数式传参,即
() => props.onGetMsg(sonMsg)
- 父组件执行getMsg方法,将响应式数据msg的值设置为实参sonMsg
三、兄弟组件通讯
和vue一样,大概有好多种方法,但是在vue中,我最喜欢用的是全局事件总线,vue2中是eventbus,vue3中是第三方插件mitt.js,其次是全局状态管理工具pinia/vuex,其他几乎不用。现在看看react
第一种是状态提升,将子A数据传给父,然后父传给子B,这种在项目实际开发中几乎不会用到,不再写例子了
1、context机制实现跨层级组件通讯
广义的兄弟组件,其实一个项目中的任意两个组件都可以称之为兄弟组件,所以直接使用跨层级组件通讯应该理论上可以解决所有组件之间的通讯
实现步骤:
- 使用createContext方法创建一个上下文对象Ctx
- 在顶层组件中通过Ctx.provide组件提供数据
- 在底层组件(B)中通过useContext钩子获取要使用的数据
javascript
import { useState, useRef, createContext, useContext } from "react";
// 1、使用createContext方法创建一个上下文对象Ctx
const MsgContext = createContext();
const Middle = () => {
return (
<div
style={{
height: "600px",
width: "800px",
background: "#fff",
margin: "20px",
}}
>
中间层组件
<Inner></Inner>
</div>
);
};
const Inner = () => {
// 3、useContext钩子接收数据,并在需要的地方渲染数据
const msg = useContext(MsgContext);
return (
<div
style={{
height: "400px",
width: "600px",
background: "#2edfa3",
margin: "20px",
}}
>
内层组件,msg来自<span style={{color: 'red'}}>{msg}</span>
</div>
);
};
function App() {
const msg = "根组件";
return (
<div
style={{
height: "800px",
width: "1200px",
background: "#eee",
}}
>
// 2、在顶层组件中通过Ctx.provide组件提供数据,value值就是要传递的数据,然后把所有内容都放到Ctx标签下
<MsgContext.Provider value={msg}>
{msg}
<Middle></Middle>
</MsgContext.Provider>
</div>
);
}
export default App;
效果,红色文字即来自根组件的数据:
2、redux状态管理工具
类似vuex,对需要全局使用的数据进行管理,但是使用起来相当的麻烦
redux并不是react专用的,可以脱离框架单独使用,首先看看脱离框架该怎么用
redux使用分5步
- 定义reducer函数
- 使用reducer函数生成store实例
- 通过store实例的subscribe订阅数据变化
- 通过store实例的dispatch函数提交action更改状态
- 通过store实例的getState方法获取最新状态更新到视图中
流程如下:
以下实现了一个数字加减的功能,相关代码:
html
<button id="decrement">-</button>
<span id="count">0</span>
<button id="increment">+</button>
<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
<script>
// 1. 定义reducer函数
// 作用: 根据不同的action对象,返回不同的新的state
// state: 管理的数据初始状态
// action: 对象 type 标记当前想要做什么样的修改
function reducer (state = { count: 0 }, action) {
// 数据不可变:基于原始状态生成一个新的状态
if (action.type === 'INCREMENT') {
return { count: state.count + 1 }
}
if (action.type === 'DECREMENT') {
return { count: state.count - 1 }
}
return state
}
// 2. 使用reducer函数生成store实例
const store = Redux.createStore(reducer)
// 3. 通过store实例的subscribe订阅数据变化
// 回调函数可以在每次state发生变化的时候自动执行
store.subscribe(() => {
console.log('state变化了', store.getState())
document.getElementById('count').innerText = store.getState().count // 第5步
})
// 4. 通过store实例的dispatch函数提交action更改状态
const inBtn = document.getElementById('increment')
inBtn.addEventListener('click', () => {
// 增
store.dispatch({
type: 'INCREMENT'
})
})
const dBtn = document.getElementById('decrement')
dBtn.addEventListener('click', () => {
// 减
store.dispatch({
type: 'DECREMENT'
})
})
// 5. 通过store实例的getState方法获取最新状态更新到视图中
</script>
如何在react中使用redux呢?
首先需要安装两个插件-redux toolkit和react-redux
(1)创建store
与vuex类似,采用组件化编程思路,按照插件后,在src目录下创建store文件夹,store文件夹下创建modules文件夹和index.js文件,modules文件夹放状态管理相关的文件,index.js集成所有的状态数据并对外暴露,文件结构如下:
└─modules
└─channnelStore.js
└─couterStore.js
└─index.js
以counterStore为例,代码如下:
javascript
import { createSlice } from "@reduxjs/toolkit";
const couterStore = createSlice({
name: "counter",
// 初始化state
initialState: {
count: 0,
},
// 修改state的方法 均为同步方法
reducers: {
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
},
},
});
// 解构actions
const { increment, decrement } = couterStore.actions;
// 获取reducer
const reducer = couterStore.reducer;
// 导出action
export { increment, decrement };
// 导出reducer
export default reducer;
(2)组合store
index.js中对多个store进行组合,代码如下:
javascript
import { configureStore } from "@reduxjs/toolkit";
import counterStore from './modules/counterStore'
const store = configureStore({
reducer: {
couter: counterStore
}
})
export default store
(3)根组件app中挂载store
在vue中,创建项目的时候,一般会提示是否使用vuex或pinia,如果勾选了,生成的main.js文件就会直接挂载pinia或者vuex,因为现在是手动安装的redux插件,所以需要手动去挂载redux,专业的说法是用react-redux把redux和react链接起来,把创建好的store实例注入到应用中
main.jsx(我创建项目用的vite,所以生成的都是jsx文件)中的写法:
javascript
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import store from "./store/index.js";
import { Provider } from "react-dom";
ReactDOM.createRoot(document.getElementById("root")).render(
<Provider store={store}>
<App />
</Provider>
);
(4)组件中使用store
需要使用钩子-useSelector,代码如下:
javascript
import { useSelector } from "react-redux";
function App() {
const msg = "根组件";
const { count } = useSelector((state) => state.counter);
return (
<div
style={{
height: "800px",
width: "1200px",
background: "#eee",
}}
>
{count}
</div>
);
}
export default App;
注意看这行代码:
javascript
const { count } = useSelector((state) => state.counter);
把名为counter的store解构出来,counter中对应的状态名为count,非常绕,比vuex和pinia都要难懂,说实话,没理解,但写法是固定的
(5)组件中修改状态数据
需要借助另一个钩子-useDispatch,用于生成提交action对象的dispatch函数,看看写法
javascript
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement } from "./store/modules/counterStore";
function App() {
const msg = "根组件";
const { count } = useSelector((state) => state.counter);
const dispatch = useDispatch()
return (
<div
style={{
height: "800px",
width: "1200px",
background: "#eee",
}}
>
<button onClick={() => dispatch(decrement())}>-</button>
<span style={{marginLeft: '10px', marginRight: '10px'}}>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
</div>
);
}
export default App;
其实就是在点击事件上添加了store中的action方法,只不过是通过useDispatch钩子生成的dispatch实例调用的
页面效果:
(6)action传递参数
以上5步是redux的主要使用方法,不过有时候在使用action方法时,需要传递参数,比如,点击减按钮,要-5,点击加按钮,要+10
方法就是在reducers的同步方法中添加action对象参数,组件上调用方法时,方法传递参数,action.payload就是实际的参数,如下图
页面上的效果
(7)异步状态操作
感觉这里异常复杂,我先上一下代码:
channelStore.js中的代码:
javascript
import { createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const channelStore = createSlice({
name: "channel",
initialState: {
channelList: ["matlab"],
},
reducers: {
setChannel(state, action) {
state.channelList = action.payload;
},
},
});
// 解构actions
const { setChannel } = channelStore.actions;
// 单独封装异步请求函数
const getChannelList = () => {
return async (dispatch) => {
const res = await axios.get("http://127.0.0.1:3030/api/test/channel");
dispatch(setChannel(res.data.channels));
};
};
// 获取reducer
const reducer = channelStore.reducer;
// 导出action
export { getChannelList };
// 导出reducer
export default reducer;
大体上与同步方法相似,但是导出的action却是单独封装的getChannelList方法,这个方法返回一个异步执行的函数,执行获取数据的代码,参数dispatch理论上应该是个方法,初次学习的时候,没能明白这是什么意思
接下来就是在index.js中注册这个store,代码如下:
javascript
import { configureStore } from "@reduxjs/toolkit";
import counterStore from "./modules/counterStore";
import channelStore from "./modules/channelStore";
const store = configureStore({
reducer: {
counter: counterStore,
channel: channelStore
},
});
export default store;
只是多了导入和注册的过程
然后看组件中怎么用,先上完整代码
javascript
import { useSelector, useDispatch } from "react-redux";
import {
increment,
decrement,
incrementNum,
decrementNum,
} from "./store/modules/counterStore";
import { getChannelList } from "./store/modules/channelStore";
import { useEffect } from "react";
function App() {
const { count } = useSelector((state) => state.counter);
const { channelList } = useSelector((state) => state.channel);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getChannelList())
}, [dispatch]);
return (
<div
style={{
height: "800px",
width: "1200px",
background: "#eee",
}}
>
<div style={{ background: "#425066", padding: "20px" }}>
<button onClick={() => dispatch(decrementNum(5))}>-5</button>
<button onClick={() => dispatch(decrement())}>-</button>
<span style={{ marginLeft: "10px", marginRight: "10px" }}>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(incrementNum(10))}>+10</button>
</div>
<div style={{ background: "#177cb0", padding: "20px" }}>
<ul>
{channelList.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
</div>
);
}
export default App;
确实很难理解,我来尝试一步步理解这个过程,只解释和之前代码差异化的地方
-
import { getChannelList } from "./store/modules/channelStore";
从store中引入异步方法 -
const { channelList } = useSelector((state) => state.channel);
导入并结构需要全局状态管理的数据 -
最难理解的地方是useEffect的使用
javascriptuseEffect(() => { dispatch(getChannelList()) }, [dispatch]);
useEffect等于vue中的onMounted+watch,在dom渲染完成后获取数据,同时监听数据的变化,也就是说dispatch(getChannelList())是获取数据的方法,dispatch是要监听的对象,但dispatch明明是个方法,很难理解。不过不要紧,可以把这里的[dispatch]换成空数组[],并不影响结果。
重点是,这里的dispatch和getChannelList方法中的dispatch有什么关系,百思不得其解,问了下AI(claude-3-haiku),它给的解释如下:
getChannelList
是一个 Redux Action Creator 函数,它返回一个异步的 thunk 函数。这个 thunk 函数内部会使用axios
发起一个 HTTP 请求,获取频道列表数据,然后使用dispatch
函数分派setChannel
action 来更新 Redux store 中的状态。- 在
App
组件中,你使用了useEffect
hook,并在useEffect
中调用了dispatch(getChannelList())
。这是因为getChannelList
是一个 Action Creator 函数,它返回了一个 thunk 函数,需要使用dispatch
函数来执行这个 thunk 函数,从而触发异步的数据获取操作。
换句话说,
getChannelList
中的dispatch
是用来分派setChannel
action 的,而useEffect
中的dispatch
是用来执行getChannelList
这个 thunk action creator 的。这样做的好处是,你可以将异步的数据获取逻辑封装在 Action Creator 函数中,使组件代码更加简洁和可读。同时,也可以在组件中轻松地触发这个异步操作,并在组件生命周期的适当时机(如组件首次渲染时)执行数据获取。
问:但是getChannelList 中的dispatch只是个形参而已,调用getChannelList 时,这个形参从哪里来?
答:你说得很对,
getChannelList
中的dispatch
参数确实只是一个形参,那它是从哪里来的呢?这要归功于 Redux 的 thunk 中间件。
当你使用
configureStore
创建 Redux store 时,你可以配置一些中间件,其中就包括 thunk 中间件。thunk 中间件会拦截你分派的 action,如果这个 action 是一个函数(也就是 thunk 函数),它就会执行这个函数,并将dispatch
函数作为参数传入。所以在
getChannelList
这个 thunk 函数中,dispatch
参数就是由 thunk 中间件注入进来的。当你在组件中使用useDispatch
分派getChannelList
时,Redux 会自动执行这个 thunk 函数,并将dispatch
参数传入。这就是为什么
getChannelList
中可以使用dispatch
函数,尽管它只是一个形参。thunk 中间件会在执行 thunk 函数时,自动将dispatch
函数注入进去。所以,
getChannelList
中的dispatch
参数是由 Redux 的 thunk 中间件提供的,而不是你自己传入的。希望这个解释有助于你理解 Redux 中 thunk 函数的工作原理。如果还有其他疑问,欢迎随时问我。问:useEffect中为什么要监听dispatch,这个dispatch又是什么
答:在你的代码中,
useEffect
中并没有监听dispatch
函数。javascriptuseEffect(() => { dispatch(getChannelList()) }, [dispatch]);
这里的
[dispatch]
是useEffect
的依赖数组,它的作用是确保useEffect
回调函数只在dispatch
函数发生变化时才会重新执行。这里的
dispatch
是从useDispatch
hook 中获取的 Redux store 的dispatch
函数。useDispatch
是 React-Redux 库提供的一个 hook,它可以让你在组件中访问 Redux store 的dispatch
函数,从而可以在组件中分派 Redux actions。所以在你的
useEffect
中,你是在调用dispatch(getChannelList())
,这样可以触发getChannelList
这个 thunk action creator,从而获取频道列表数据并更新 Redux store。useEffect
中的依赖数组[dispatch]
的作用是,当dispatch
函数发生变化时(比如你切换到另一个组件,该组件有不同的dispatch
函数),useEffect
回调函数会重新执行,从而可以获取最新的频道列表数据。总之,这里的
dispatch
就是从useDispatch
获取的 Redux store 的dispatch
函数,它没有被监听,只是作为依赖项出现在useEffect
的依赖数组中。希望这个解释有助于你理解这段代码。如果还有其他问题,欢迎继续询问。
其实我并没有理解,,,
最后页面效果如下:
3、events事件总线
类似vue中的mitt
react中使用最多的是events库,用到了再补充相应的知识点