我有两个单选按钮,当我单击其中一个按钮时,我希望UI的状态立即更改,然后调用后端保存此更改,然后如果后端发生故障,则恢复UI。
下面是React组件:
class SomeComponent extends React.Component {
handleModeChange(event) {
const { saveFeatureActivationMode } = this.props;
const {target} = event;
const selectedMode = target.value;
const { currentMode } = this.state;
this.setState(
{
currentMode: selectedMode
},
() => saveMode(
selectedMode,
() => revertMode(currentMode),
)
);
}
revertMode(currentMode) {
this.setState({ currentMode })
}
...
}
const mapDispatchToProps = dispatch => ({
saveMode: (mode, onError) =>
dispatch(updateMode(mode, onError)),
});
那么我的updateMode动作创建者是:
const updateMode = (mode, onError) => ({
type: SEND,
payload: {
url: `/api/update/mode`,
options: {
body: {
mode,
},
method: 'PUT',
},
onSuccess: () => (
showNotification("Success!");
),
onError: () => {
onError();
return showGlobalError("Error occurred!");
},
},
});
请注意,SEND
动作类型将被一些redux中间件拾取,并实际发送api请求。
现在,我可以立即看到这个设计打破了redux的两个规则,1)单向流,2)将不可序列化的函数传递给操作创建器。在模式改变时乐观地设置UI,但在失败时恢复UI,这是最好的方法吗?
我有很多关于如何处理这个问题的想法,就基于钩子的方法而言,确定通过props传递给钩子的逻辑以及钩子功能固有的逻辑取决于你的应用程序中可以重用这些逻辑的用例。
我在这里想出的是一个钩子来处理"pending"通过API更新并存储在redux中的任何值。
useOpportunisticValue
参数mapStateToValue
(state) => value
从状态中选择当前值的redux选择器。publishChanges
(pendingValue) => Promise<newValue>
一个async函数,它调用API并返回后端设置的新值。你可以忽略获取的响应并返回相同的值,但是在这里返回一个值可以支持API执行某种规范化的情况,例如挂起值和新值不完全相同。
dispatchChanges
(dispatch, value) => void
是一个回调,它给你访问新的值,dispatch
这样你就可以用适当的动作调用调度。返回[value, onChange]
一个setState
样式的元组
value
应该在你的组件中显示的值,允许你的组件不知道该值是pending还是permanent。
onChange
(newValue) => void
当你想要更新值时调用的回调。该钩子提取出确定哪个值是当前值的逻辑。调用onChange
将导致value
立即更改,但如果API更新失败,value
可能会在稍后恢复。
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
const useOpportunisticValue = (
mapStateToValue,
publishChanges,
dispatchChanges
) => {
// state stores the pending value when changes are pending
// or null at all other times
const [pendingValue, setPendingValue] = useState(null);
// select current value from redux store
const storedValue = useSelector(mapStateToValue);
// the value returned to the component is the pending value when changes are pending
// or the value from redux otherwise
const value = pendingValue === null ? storedValue : pendingValue;
// onChange handler returned to the component updates the pending value
const onChange = (value) => {
setPendingValue(value);
//TODO: cancel subscriptions to previous updates
};
const dispatch = useDispatch();
// hook listens for changes to the pending value
useEffect(
() => {
// this value is used to prevent error "called setState on an unmounted component"
// if the component using this hook unmounts before the API returns
let isSubscribed = true;
const execute = async () => {
// only do anything if we have an actual value
if (pendingValue === null) return;
try {
// publish the changes to the API
const newValue = await publishChanges(pendingValue);
// only dispatch the changes to redux after API success
dispatchChanges(dispatch, newValue);
} catch (e) {
// clear the pending changes in the case of a failure
if (isSubscribed) {
setPendingValue(null);
}
}
};
execute();
// return cleanup function
return () => {
isSubscribed = false;
};
},
// needs extra deps to pass eslint with exhaustive checks
// I am leaving off publishChanges and dispatchChanges for no so that you don't have to memoize
[pendingValue]
);
// return a [state, setState] tuple
return [value, onChange];
};
<<h3>打印稿版本/h3>import { useEffect, useState } from "react";
import { Dispatch } from "redux";
import { useDispatch, useSelector, DefaultRootState } from "react-redux";
const useOpportunisticValue = <T, State = DefaultRootState>(
mapStateToValue: (state: State) => T,
publishChanges: (value: T) => Promise<T>,
dispatchChanges: (dispatch: Dispatch, value: T) => void
): [T, (value: T) => void] => {
const [pendingValue, setPendingValue] = useState<T | null>(null);
...
使用显然,你会想要使用选择器函数、动作创建器、API客户端等来提取一些逻辑。
const Mode = () => {
const [mode, onChange] = useOpportunisticValue(
(state: MyState) => state.mode,
async (mode) => {
const res = await axios.request({
url: `/api/update/mode`,
data: { mode },
method: 'PUT',
});
return res.data.mode;
},
(dispatch, mode) => dispatch({
type: "SET_MODE",
payload: mode
})
)
// do some actual better rendering
return (
<div>Mode: {mode} </div>
)
}