【问题标题】:Special actions on React state variablesReact 状态变量的特殊操作
【发布时间】:2021-05-19 04:46:06
【问题描述】:

我有一个带有需要特定操作的状态变量的 React 组件。例如,考虑一个显示用户配置文件列表的组件,用户可以切换到另一个配置文件或创建一个新配置文件。状态变量是用户配置文件列表,第二个变量是当前选择的配置文件;该组件可以添加一个新的配置文件(这比仅仅“设置”一个新的配置文件列表更具体),或者它可以更改当前选择的配置文件。

我的第一个想法是有两个useState 挂钩,一个用于列表,一个用于当前配置文件。但是,其中一个问题是我想存储当前配置文件的 id,它引用列表中的配置文件之一,这意味着两个状态变量是相互依赖的。另一个问题是,拥有一个通用的setProfiles 状态更改函数对我来说有点过于“开放”:添加逻辑可能非常具体,我想封装它。

所以我想出了这个解决方案:一个管理两个状态变量及其设置器的自定义挂钩,它将公开两个值(列表和当前 id)及其适当的操作(添加新配置文件和选择配置文件)。

这是钩子的代码:

export const useProfileData = () => {
    const [ profiles, setProfiles ] = useState([]);
    const [ currentProfileID, setCurrentProfileID ] = useState(null);
    const [ currentProfile, setCurrentProfile ] = useState(null);

    useEffect(() => {
        // This is actually a lazy deferred data fetch, but I'm simplifying for the sake of brevity
        setProfiles(DataManager.getProfiles() || [])
    }, [])

    useEffect(() => {
        if (!profiles) {
            setCurrentProfile(null);
            return;
        }
        const cp = profiles.find(p => p.ID === currentProfileID);
        setCurrentProfile(cp);
    }, [ currentProfileID, profiles ])

    return { 
        currentProfile: currentProfile, 
        profiles: profiles,
        setCurrentProfileID: i_id => setCurrentProfileID(i_id),
        addNewProfile: i_profile => {
            profiles.push(i_profile);
            setProfiles(profiles);
            DataManager.addNewProfile(i_profile); // this could be fire-and-forget
        },
    };
};

使用三种状态:列表、当前配置文件 id 和当前配置文件(作为对象)。该列表是在挂载时检索的(当前 id 也应该是,但为简洁起见,我省略了它)。当前配置文件从不直接从外部设置:更改它的唯一方法是更改​​由第二个useEffect 管理的 id 或列表。而更改 id 的唯一方法是通过暴露的 setCurrentProfileID 函数。

添加新配置文件由公开的addNewProfile 函数管理,该函数应将新配置文件添加到状态列表中,更新状态列表,并将新配置文件添加到持久数据管理器中。

我的第一个问题是:设计这样的钩子可以吗? 从一般软件设计的角度来看,这段代码提供了封装、关注点分离和正确的状态管理。我不确定这在像 React 这样的函数式世界中是否合适。

我的第二个问题是:为什么我的组件(使用useProfileData)在调用addNewProfile 时没有更新? 例如:

const ProfileSelector = (props) => {
    const [ newProfileName, setNewProfileName ] = useState('');
    const { profiles, currentProfile, setCurrentProfileID, addNewProfile } = useProfileData();

    function createNewProfile() {
        addNewProfile({
            name: newProfileName,
        });
    }

    return (
        <div>
            <ProfilesList profiles={profiles} onProfileClick={pid => setCurrentProfileID(pid)} />
            <div>
                <input type="text" value={newProfileName} onChange={e => setNewProfileName(e.target.value)} />
                <Button label="New profile" onPress={() => createNewProfile()} />
            </div>
        </div>
    );
};

ProfilesListButton 是在别处定义的组件。

当我点击按钮时,一个新的配置文件被添加到持久数据管理器中,但profiles 没有更新,ProfilesList 也没有更新(当然)。

我要么实现了一些错误,要么这不是一个可以在 React 中工作的范例。我能做什么?

编辑

正如@thedude 所建议的,我尝试使用减速器。这是我的减速器的(存根):

const ProfilesReducer = (state, action) => {
    const newState = state;
    switch (action.type) {
        case 'addNewProfile':
            {
                const newProfile = action.newProfile;
                newState.profiles.push(newProfile);
                DataManager.addNewProfile(newProfile);
            }
            break;
        default:
            throw new Error('Unexpected action type: ' + action.type);
    }
    return newState;
}

在我调用它 (profilesDispatch({ type: 'addNewProfile', newProfile: { name: 'Test' } });) 之后,没有检测到 profilesState.profiles 的变化 - 或者至少,渲染从未被触发,也没有任何效果。但是,对 DataManager 的调用已完成,新的配置文件已被保留。

【问题讨论】:

  • 你考虑过useReducer吗?
  • @thedude useReducer 会给我一个状态变量;我需要两个。虽然我可以使用带有我的两个变量的单个对象作为其属性,但当减速器状态的属性发生变化而不是整个减速器的状态时,是否会通知状态更新?
  • useReducer 的重点是为所有状态突变提供一个中心位置,这在处理相互依赖的数据时很方便。您的状态可以有多个值,并且由于所有状态更改都通过 reducer,您可以更轻松地管理它
  • @thedude 那我试试reducer,谢谢!
  • @thedude 我尝试了减速器,但仍然没有成功。我在上面贴了reducer:我做错了吗?

标签: reactjs


【解决方案1】:

你不应该改变你的状态,即使是在 reducer 函数中。

来自文档:

如果您从 Reducer Hook 返回与当前状态相同的值,React 将退出而不渲染子级或触发效果。 (React 使用 Object.is 比较算法。)

更改你的 reducer 以返回一个新对象:

const ProfilesReducer = (state, action) => {
    switch (action.type) {
        case 'addNewProfile':
            {
                const newProfile = action.newProfile;
                return {...state, profiles: [...state.profiles, newProfile]}
            }
            break;
        default:
            throw new Error('Unexpected action type: ' + action.type);
    }
    return state;
}

也不是说 reducer 不应该有副作用,如果你想根据状态变化执行一些操作,请使用 useEffect 钩子。

例如: DataManager.addNewProfile(newProfile) 不应从 reducer 调用

【讨论】:

  • 有没有可能做减速器的作用?您的解决方案意味着我必须使用减速器 效果。我可以在自定义挂钩中加入它们吗?
  • 还有一件事:我在事件处理程序中调用的函数会产生副作用吗?我可以在事件处理程序中执行DataManager.addNewProfile 吗?如果不是,我必须有效地接收新状态(包括数组末尾的新配置文件 - 或者可能不:数组可能已以其他方式更改)并检测新配置文件并保存它。
  • 最后的想法:如何异步加载我的初始 reducer 值?
  • 是的,您可以在事件处理程序中产生副作用。其他问题请见stackoverflow.com/questions/53146795/…
  • 是否可以定义一个自定义钩子,该钩子返回一个带有方法的对象(如我原来的帖子中),其中一些有副作用?这样,对dispatch 的调用实际上将被包装和封装。
猜你喜欢
  • 2015-05-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-14
  • 2021-05-21
  • 2018-11-23
相关资源
最近更新 更多