Javascript 如何正确地将useReducer操作传递给子级,而不会导致不必要的渲染
我不太清楚使用Javascript 如何正确地将useReducer操作传递给子级,而不会导致不必要的渲染,javascript,reactjs,react-hooks,Javascript,Reactjs,React Hooks,我不太清楚使用useReducerhook进行数据管理的最佳方式。我的主要目标是将样板文件减少到最低限度并保持代码可读性,同时在性能方面使用最佳方法并防止不必要的重新呈现 设置 我创建了我的应用程序的一个简化示例,基本上它是一个组件-一个可以选择它们的项目列表,以及一个可以切换项目组和重新加载数据集的组件 List.js 在一个简单的例子中,它看起来并不太糟糕,但是当您有几十个可能的操作时,所有这些操作都包装在带有deps数组的useCallback中,这似乎是不对的 它还需要在useffect
useReducer
hook进行数据管理的最佳方式。我的主要目标是将样板文件减少到最低限度并保持代码可读性,同时在性能方面使用最佳方法并防止不必要的重新呈现
设置
我创建了我的应用程序的一个简化示例,基本上它是一个
组件-一个可以选择它们的项目列表,以及一个可以切换项目组和重新加载数据集的
组件
List.js
在一个简单的例子中,它看起来并不太糟糕,但是当您有几十个可能的操作时,所有这些操作都包装在带有deps数组的useCallback
中,这似乎是不对的
它还需要在useffect
中添加更多的dep(这是另一个问题)
下面是一个带有useCallback
的示例
问题2
我无法在之外完全提取reducer操作函数
但它是最优的,还是我应该使用一些完全不同的方法
你们是怎么用的?React文档和指南很好,有计数器增量和待办事项列表,但您如何在实际应用程序中实际使用它?React redux还通过调用dispatch来包装所有操作;这在使用connect
HOC时被抽象掉,但在使用useDispatch
hook时仍然是必需的。异步操作通常有一个函数签名(…args)=>dispatch=>{}
,其中操作创建者返回一个接受redux提供的调度函数的函数,但redux需要中间件来处理这些函数。由于您实际上并没有使用Redux,所以您需要自己处理这个问题,可能需要使用这两种模式的组合来实现类似的用法
我建议作出以下修改:
分离和隔离动作创建者,它们应该是返回动作对象的函数(或异步动作函数)
创建处理异步操作的自定义分派函数
组件渲染时正确记录(即在useffect
hook的提交阶段和not组件主体的任何渲染阶段。请参阅)
将自定义分派功能传递给子级,在子级中导入操作…在子级中分派操作
仅有条件地呈现加载程序
组件。当呈现加载程序
和列表
中的一个或另一个时,另一个将卸载
Actions(Actions.js)
useAncyReducer.js
const asyncDispatch = (dispatch) => (action) =>
action instanceof Function ? action(dispatch) : dispatch(action);
export default (reducer, initialArg, init) => {
const [state, syncDispatch] = React.useReducer(reducer, initialArg, init);
const dispatch = React.useMemo(() => asyncDispatch(syncDispatch), []);
return [state, dispatch];
};
为什么usemo
不需要依赖于useReducer
dispatch函数
注意
React保证分派
功能标识是稳定的,不会
重新渲染时的更改。这就是为什么可以安全地从useffect
或useCallback
依赖项列表
我们还希望提供一个稳定的调度
功能参考
App.js
import React, { useEffect } from "react";
import useReducer from "./useAsyncReducer";
import Controls from "./Controls";
import List from "./List";
import Loader from "./Loader";
import { ItemGroups } from "./constants";
import {
FETCH_START,
FETCH_SUCCESS,
SET_GROUP,
SELECT_ITEM,
DESELECT_ITEM
} from "./constants";
import { fetchItems } from "./actions";
export default function App() {
const [state, dispatch] = useReducer(itemsReducer, {
items: [],
selected: [],
group: ItemGroups.PEOPLE,
isLoading: false
});
const { items, group, selected, isLoading } = state;
useEffect(() => {
console.log("use effect on group change");
dispatch(fetchItems(group));
}, [group]);
React.useEffect(() => {
console.log("<App /> render");
});
return (
<div className="App">
<Controls {...{ group, dispatch }} />
{isLoading && <Loader />}
<List {...{ items, selected, dispatch }} />
</div>
);
}
import React,{useffect}来自“React”;
从“/UseAncyReducer”导入useReducer;
从“/Controls”导入控件;
从“/List”导入列表;
从“/Loader”导入加载程序;
从“/constants”导入{ItemGroups};
进口{
开始,
取得成功,
集欧集团,
选择_项,
取消选择项目
}从“/常量”;
从“/actions”导入{fetchItems};
导出默认函数App(){
const[state,dispatch]=useReducer(itemsReducer{
项目:[],
选定:[],
组:ItemGroups.PEOPLE,
孤岛加载:false
});
const{items,group,selected,isLoading}=state;
useffect(()=>{
console.log(“对集团变更的使用影响”);
调度(取件(组));
}[组];
React.useffect(()=>{
console.log(“在useCallback
中包装您的“动作创建者”,以提供稳定的动作回调引用,(1)因为这是它的设计目的,它不是“黑客”,以及(2)IMO是所示选项中最干净的代码。每个渲染周期都会重新声明动作创建者。是否有一些子组件重新渲染的问题?您应该反转您的动作,以便dispatch
包装返回的动作对象,然后您可以从外部定义动作创建者,它们不会成为渲染依赖项。您可以simp把dispatch
传递给孩子们。是的,我也更喜欢第二种方法,我知道使用useCallback不是一种黑客行为,但它添加了太多不必要的代码,这是我想要避免的。至于反转操作-这是一个好主意,但并不适用于所有情况,因为有些操作函数会触发多个ac重新渲染-是的,我认为这是一个问题。我知道React实际上并没有修改所有渲染的DOM,但在应用程序的每个部分添加这种低效最终会增加。对我来说,这意味着知道它不会做任何愚蠢的事。:)
import React, { useEffect, useReducer } from "react";
import Controls from "./Controls";
import List from "./List";
import Loader from "./Loader";
import { ItemGroups } from "./constants";
import {
FETCH_START,
FETCH_SUCCESS,
SET_GROUP,
SELECT_ITEM,
DESELECT_ITEM
} from "./constants";
import fetchItemsFromAPI from "./api";
import "./styles.css";
const itemsReducer = (state, action) => {
const { type, payload } = action;
console.log(`reducer action "${type}" dispatched`);
switch (type) {
case FETCH_START:
return {
...state,
isLoading: true
};
case FETCH_SUCCESS:
return {
...state,
items: payload.items,
isLoading: false
};
case SET_GROUP:
return {
...state,
selected: state.selected.length ? [] : state.selected,
group: payload.group
};
case SELECT_ITEM:
return {
...state,
selected: [...state.selected, payload.id]
};
case DESELECT_ITEM:
return {
...state,
selected: state.selected.filter((id) => id !== payload.id)
};
default:
throw new Error("Unknown action type in items reducer");
}
};
export default function App() {
const [state, dispatch] = useReducer(itemsReducer, {
items: [],
selected: [],
group: ItemGroups.PEOPLE,
isLoading: false
});
const { items, group, selected, isLoading } = state;
const fetchItems = (group) => {
dispatch({ type: FETCH_START });
fetchItemsFromAPI(group).then((items) =>
dispatch({
type: FETCH_SUCCESS,
payload: { items }
})
);
};
const setGroup = (group) => {
dispatch({
type: SET_GROUP,
payload: { group }
});
};
const selectItem = (id) => {
dispatch({
type: SELECT_ITEM,
payload: { id }
});
};
const deselectItem = (id) => {
dispatch({
type: DESELECT_ITEM,
payload: { id }
});
};
useEffect(() => {
console.log("use effect on group change");
fetchItems(group);
}, [group]);
console.log("<App /> render");
return (
<div className="App">
<Controls {...{ group, fetchItems, setGroup }} />
{isLoading ? (
<Loader />
) : (
<List {...{ items, selected, selectItem, deselectItem }} />
)}
</div>
);
}
const setGroup = useCallback(
(group) => {
dispatch({
type: SET_GROUP,
payload: { group }
});
},
[dispatch]
);
// ...
export const fetchItems = (dispatch, group) => {
dispatch({ type: FETCH_START });
fetchItemsFromAPI(group).then((items) =>
dispatch({
type: FETCH_SUCCESS,
payload: { items }
})
);
};
// ...
import { fetchItems } from './actions';
const Child = ({ dispatch, group }) => {
fetchItems(dispatch, group);
// ...
};
// ...
const App = () => {
const [{ items, group, selected, isLoading }, dispatch] = useReducer(
itemsReducer,
itemReducerDefaults
);
useEffect(() => {
fetchItems(dispatch, group);
}, [group, dispatch]);
return (
<div className="App">
<Controls {...{ group, dispatch }} />
{isLoading ? <Loader /> : <List {...{ items, selected, dispatch }} />}
</div>
);
};
import {
FETCH_START,
FETCH_SUCCESS,
SET_GROUP,
SELECT_ITEM,
DESELECT_ITEM
} from "./constants";
import fetchItemsFromAPI from "./api";
export const setGroup = (group) => ({
type: SET_GROUP,
payload: { group }
});
export const selectItem = (id) => ({
type: SELECT_ITEM,
payload: { id }
});
export const deselectItem = (id) => ({
type: DESELECT_ITEM,
payload: { id }
});
export const fetchItems = (group) => (dispatch) => {
dispatch({ type: FETCH_START });
fetchItemsFromAPI(group).then((items) =>
dispatch({
type: FETCH_SUCCESS,
payload: { items }
})
);
};
const asyncDispatch = (dispatch) => (action) =>
action instanceof Function ? action(dispatch) : dispatch(action);
export default (reducer, initialArg, init) => {
const [state, syncDispatch] = React.useReducer(reducer, initialArg, init);
const dispatch = React.useMemo(() => asyncDispatch(syncDispatch), []);
return [state, dispatch];
};
import React, { useEffect } from "react";
import useReducer from "./useAsyncReducer";
import Controls from "./Controls";
import List from "./List";
import Loader from "./Loader";
import { ItemGroups } from "./constants";
import {
FETCH_START,
FETCH_SUCCESS,
SET_GROUP,
SELECT_ITEM,
DESELECT_ITEM
} from "./constants";
import { fetchItems } from "./actions";
export default function App() {
const [state, dispatch] = useReducer(itemsReducer, {
items: [],
selected: [],
group: ItemGroups.PEOPLE,
isLoading: false
});
const { items, group, selected, isLoading } = state;
useEffect(() => {
console.log("use effect on group change");
dispatch(fetchItems(group));
}, [group]);
React.useEffect(() => {
console.log("<App /> render");
});
return (
<div className="App">
<Controls {...{ group, dispatch }} />
{isLoading && <Loader />}
<List {...{ items, selected, dispatch }} />
</div>
);
}
import React, { memo } from "react";
import { ItemGroups } from "./constants";
import { setGroup, fetchItems } from "./actions";
const Controls = ({ dispatch, group }) => {
React.useEffect(() => {
console.log("<Controls /> render");
});
return (
<div className="Controls">
<label>
Select group
<select
value={group}
onChange={(e) => dispatch(setGroup(e.target.value))}
>
<option value={ItemGroups.PEOPLE}>{ItemGroups.PEOPLE}</option>
<option value={ItemGroups.TREES}>{ItemGroups.TREES}</option>
</select>
</label>
<button onClick={() => dispatch(fetchItems(group))}>Reload data</button>
</div>
);
};
import React, { memo } from "react";
import { deselectItem, selectItem } from "./actions";
const List = ({ dispatch, items, selected }) => {
React.useEffect(() => {
console.log("<List /> render");
});
return (
<ul className="List">
{items.map(({ id, name }) => (
<li key={`item-${name.toLowerCase()}`}>
<label>
<input
type="checkbox"
checked={selected.includes(id)}
onChange={(e) =>
dispatch((e.target.checked ? selectItem : deselectItem)(id))
}
/>
{name}
</label>
</li>
))}
</ul>
);
};
const Loader = () => {
React.useEffect(() => {
console.log("<Loader /> render");
});
return <div>Loading data...</div>;
};