简体   繁体   English

React 自定义钩子重新渲染

[英]React custom hook re-rendering

I am learning react and right now I have a react app with users and movies, where you can see info about movies whenever you are logged in. If you are logged in as an admin you get access to an adminpage where you can see a list of usercards and register another admin.我正在学习反应,现在我有一个包含用户和电影的反应应用程序,您可以在登录时看到有关电影的信息。如果您以管理员身份登录,您可以访问管理页面,您可以在其中查看列表用户卡并注册另一个管理员。 This cardlist gets updated without needing to refresh.此卡片列表无需刷新即可更新。

Since the code that I wrote is not that clean, I wanted to incorporate custom hooks.由于我编写的代码不是那么干净,所以我想合并自定义挂钩。 The problem is that with the new custom hooks everything works fine except for the rendering.问题是,使用新的自定义挂钩,除了渲染之外,一切正常。 Whenever I delete a user or add a new admin, the cardlist does not get updated unless I refresh the page.每当我删除用户或添加新管理员时,卡片列表都不会更新,除非我刷新页面。

I now have a custom hook useUsers but I only use it for my input fields and toast notifcations.我现在有一个自定义挂钩useUsers ,但我只将它用于我的输入字段和 toast 通知。 I tried adding users to my useEffect in the hook but that didn't fix my problem.我尝试在挂钩中将用户添加到我的 useEffect 中,但这并没有解决我的问题。

useEffect(() => { refreshUserList(); }, [users]);

Here is my code.这是我的代码。

function useUsers() {
    const [users, setUsers] = useState([])
    const [showModal, setShowModal] = useState(false)

    const notifyUserDeleted = () => toast.success('User deleted!', {
        position: "top-right",
        autoClose: 3000,
        hideProgressBar: false,
        closeOnClick: true,
        pauseOnHover: true,
        draggable: true,
        progress: undefined,
        theme: "colored",
    });

    const [adminUsername, setAdminUsername] = useState("")
    const [adminPassword, setAdminPassword] = useState("")
    const [showAdminModal, setShowAdminModal] = useState(false)

    const notifyAddAdminSuccess = () =>
        toast.success('Admin registered!', {
            position: "top-right",
            autoClose: 3000,
            hideProgressBar: false,
            closeOnClick: true,
            pauseOnHover: true,
            draggable: true,
            progress: undefined,
            theme: "colored",
        })

    const refreshUserList = () => {
        UserAPI.getUsers()
            .then(res => {
                setUsers(res.data.users);
            })
            .catch(err => {
                console.error(err);
            });
    };

    useEffect(() => {
        refreshUserList();
    }, []);

    const createAdmin = (adminUsername, adminPassword) => {
        const registeredAdmin = {
            "username": adminUsername,
            "password": adminPassword
        };
        UserAPI.createAdmin(registeredAdmin)
            .then(() => {
                setAdminUsername("");
                setAdminPassword("");
                notifyAddAdminSuccess();
                Adminpage.refreshUserList();
            })
            .catch(err => {
                console.log(err);
                alert("Failed to register admin!");
            });
    };

    const handleRegisterAdmin = (e) => {
        e.preventDefault();
        createAdmin(adminUsername, adminPassword);
        setShowAdminModal(false);
    };

    const deleteUser = (id) => {
        UserAPI.deleteUser(id)
            .then(() => {
                notifyUserDeleted()
                refreshUserList()
            })
            .catch(error => {
                console.error(error);
            });
    };

    const handleDelete = (e) => {
        e.preventDefault();
        deleteUser(e.target.value)
        setShowModal(false)
    }

    return {
        users, setUsers, showModal, setShowModal, notifyUserDeleted, notifyAddAdminSuccess, showAdminModal, setShowAdminModal,
        adminPassword, setAdminPassword, adminUsername, setAdminUsername, refreshUserList, handleRegisterAdmin, handleDelete
    }
}

export default useUsers;

function Adminpage() {
    const { users, refreshUserList } = useUsers();

    return (
        <div className="container" style={{ display: "flex" }}>
            <UserCardList users={users} refreshUserList={refreshUserList} />
            <InputAdmin refreshUserList={refreshUserList} />
        </div>
    );
}

export default Adminpage;
function UserCardList(props) {
    return (
        <div className="container">
            <h4 style={{ margin: "3% 0 2% 0" }}>User list:</h4>
            <Bootstrap.Row>
                {
                    props.users.map(user =>
                        <UserCard key={user.id} users={user} refreshUserList={props.refreshUserList} />
                    )
                }
            </Bootstrap.Row>
        </div>
    )
}

export default UserCardList;
function UserCard(props) {
    const { showModal,
        setShowModal,
        handleDelete
    } = useUsers()

    return (
        <Bootstrap.Col className="col-lg-4 col-12">
            <Bootstrap.Card className='mb-1' style={{ height: "98%", }}>
                <Bootstrap.Card.Body>
                    <Bootstrap.Card.Text><b>User ID: </b>{props.users.id}</Bootstrap.Card.Text>
                    <Bootstrap.Card.Text><b>Username: </b>{props.users.username}</Bootstrap.Card.Text>
                    <Bootstrap.Button style={{ backgroundColor: "red", borderColor: "gray" }} onClick={() => setShowModal(true)}><RiDeleteBin5Fill style={{ backgroundColor: "red" }} /></Bootstrap.Button>
                </Bootstrap.Card.Body>
            </Bootstrap.Card>
            <Bootstrap.Modal centered show={showModal} onHide={() => setShowModal(false)}>
                <Bootstrap.Modal.Header closeButton>
                    <Bootstrap.Modal.Title>Confirm Delete</Bootstrap.Modal.Title>
                </Bootstrap.Modal.Header>
                <Bootstrap.Modal.Body>Are you sure you want to delete this user?</Bootstrap.Modal.Body>
                <Bootstrap.Modal.Footer>
                    <Bootstrap.Button variant="secondary" onClick={() => setShowModal(false)}>
                        Cancel
                    </Bootstrap.Button>
                    <Bootstrap.Button variant="danger" value={props.users.id} onClick={handleDelete}>
                        Delete
                    </Bootstrap.Button>
                </Bootstrap.Modal.Footer>
            </Bootstrap.Modal>
        </Bootstrap.Col>
    )
}

export default UserCard;

Issue问题

useEffect(() => { refreshUserList(); }, [users]);

Adding users to the useEffect hook will likely cause a render loop since refreshUserList ultimates updates the users state. Don't unconditionally update any of a hook's dependencies.users添加到useEffect挂钩可能会导致渲染循环,因为refreshUserList最终会更新users state。不要无条件地更新任何挂钩的依赖项。

React hooks also don't share state. You've two components, Adminpage and UserCard , each using separate instances of a useUsers hook each with their own state. Mutating the state in one instance of useUsers doesn't effect any other instance of useUsers . React hooks 也不共享 state。您有两个组件AdminpageUserCard ,每个组件都使用useUsers挂钩的单独实例,每个实例都有自己的 state。在一个 useUsers 实例中useUsers不会影响任何其他useUsers实例.

Solution解决方案

Move the state and logic from the useUsers to a singular React context provider and allow all instances of the useUsers hook to access the single context value.将 state 和逻辑从useUsers移动到单个 React 上下文提供程序,并允许useUsers挂钩的所有实例访问单个上下文值。

Example:例子:

export const UsersContext = React.createContext({
  adminPassword: "",
  adminUsername: "",
  handleDelete: () => {},
  handleRegisterAdmin: () => {},
  notifyAddAdminSuccess: () => {},
  notifyUserDeleted: () => {},
  setAdminPassword: () => {},
  setAdminUsername: () => {},
  setShowAdminModal: () => {},
  setShowModal: () => {},
  setUsers: () => {},
  showModal: () => {},
  showAdminModal: false,
  refreshUserList: () => {},
  users: [],
});

export const useUsers = () => React.useContext(UsersContext);

const toastOptions = {
  position: "top-right",
  autoClose: 3000,
  hideProgressBar: false,
  closeOnClick: true,
  pauseOnHover: true,
  draggable: true,
  progress: undefined,
  theme: "colored",
};
const UsersProvider = ({ children }) => {
  const [users, setUsers] = useState([]);
  const [showModal, setShowModal] = useState(false);
  const [adminUsername, setAdminUsername] = useState("");
  const [adminPassword, setAdminPassword] = useState("");
  const [showAdminModal, setShowAdminModal] = useState(false);

  const notifyUserDeleted = () =>
    toast.success('User deleted!', toastOptions);

  const notifyAddAdminSuccess = () =>
    toast.success('Admin registered!', toastOptions);

  const refreshUserList = () => {
    UserAPI.getUsers()
      .then(res => {
        setUsers(res.data.users);
      })
      .catch(err => {
        console.error(err);
      });
  };

  useEffect(() => {
    refreshUserList();
  }, []);

  const createAdmin = (username, password) => {
    const registeredAdmin = { username, password };

    UserAPI.createAdmin(registeredAdmin)
      .then(() => {
        setAdminUsername("");
        setAdminPassword("");
        notifyAddAdminSuccess();
        Adminpage.refreshUserList();
      })
      .catch(err => {
        console.log(err);
        alert("Failed to register admin!");
      });
  };

  const handleRegisterAdmin = (e) => {
    e.preventDefault();
    createAdmin(adminUsername, adminPassword);
    setShowAdminModal(false);
  };

  const deleteUser = (id) => {
    UserAPI.deleteUser(id)
      .then(() => {
        notifyUserDeleted();
        refreshUserList();
      })
      .catch(error => {
        console.error(error);
      });
  };

  const handleDelete = (e) => {
    e.preventDefault();
    deleteUser(e.target.value);
    setShowModal(false);
  }

  const value = {
    adminPassword,
    adminUsername,
    handleDelete,
    handleRegisterAdmin,
    notifyUserDeleted,
    notifyAddAdminSuccess,
    refreshUserList,
    setUsers,
    showModal,
    setShowModal,
    showAdminModal,
    setShowAdminModal,
    setAdminPassword,
    setAdminUsername,
    users,
  };

  return (
    <UsersContext.Provider value={value}>
      {children}
    </UsersContext.Provider>
  );
}

Wrap the app code with the UsersProvider component to provide the users state and callbacks.使用UsersProvider组件包装应用程序代码以提供用户 state 和回调。

<UsersProvider>
  ...
  <Adminpage />
  ...
</UsersProvider>

Now all the components rendered in UsersProvider 's sub-Reactree using the useUsers hook will access and reference the same state and callbacks.现在,使用useUsers钩子在UsersProvider的子 Reactree 中呈现的所有组件都将访问和引用相同的 state 和回调。

The trouble you face is very very common in React:) Each hook usage creates it's "own" isolated scope. Meaning that each useUsers() usage produces separate users collection (retrieved from your back-end inside useEffect) and exposes separate set of user-manipulation-specific methods (like handleDelete)您遇到的问题在 React 中非常常见:) 每个挂钩使用都会创建它的“自己的”隔离 scope。这意味着每个 useUsers() 使用都会产生单独的用户集合(从您的后端在 useEffect 中检索)并公开单独的用户集-特定于操作的方法(如 handleDelete)


First of all, before answering initial question let's reveal very very dramatic trouble you'll probably face soon首先,在回答最初的问题之前,让我们揭示一下您可能很快会遇到的非常非常大的麻烦

As far as each "useUsers" execution has it's own scope, every time you use this hook, following code will get executed:至于每个“useUsers”执行都有它自己的 scope,每次你使用这个钩子时,下面的代码都会被执行:

useEffect(() => {
    refreshUserList();  // UserAPI.getUsers()
}, []);

Now imagine, you call this hook in Adminpage component - users get loaded from the back-end.现在想象一下,您在 Adminpage 组件中调用此挂钩 - 用户从后端加载。 When users are loaded - you render UserCardList with loaded users.加载用户时 - 您使用加载的用户呈现 UserCardList。 And UserCardList renders UserCard component for each loaded user. UserCardList 为每个加载的用户呈现 UserCard 组件。 And finally each UserCard component calls "useUsers" hook you've created and each of them calls code above (useEffect -> refreshUserList) so each of UserCard component loads users from your back-end one more time:(最后,每个 UserCard 组件都会调用您创建的“useUsers”挂钩,并且每个组件都会调用上面的代码(useEffect -> refreshUserList),因此每个 UserCard 组件都会再次从您的后端加载用户:(

You see the trouble, you load 20 users, and for each of these users you load all these users again.您看到了问题所在,您加载了 20 个用户,并且对于这些用户中的每一个,您再次加载所有这些用户。 if you have 20 users - it would be 21 call on your back-end.如果您有 20 个用户 - 在您的后端将调用 21 个。 If you have 100 users - it would be 101 call on the back-end, etc... The performance of the back-end will definitely degradade very very quickly如果您有 100 个用户 - 后端将调用 101 次,等等......后端的性能肯定会非常非常快地下降

To prove this assumption please open.network tab in your browser and just reload the page.为了证明这个假设,请在浏览器中打开.network 选项卡并重新加载页面。 You'll dfefinitely see endless sequence of load-users requests...您肯定会看到无穷无尽的加载用户请求序列......


That was about the back-end, but the initial question with not working front-end when users are removed is still opened那是关于后端的,但是当用户被删除时前端不工作的最初问题仍然存在

The situation is following: you use "useUsers" in Adminpage component to render user list and you use "useUsers" in each UserCard component to call "handleDelete" whenever needed情况如下:您在 Adminpage 组件中使用“useUsers”来呈现用户列表,并在每个 UserCard 组件中使用“useUsers”以在需要时调用“handleDelete”

Each of these components loads "users" independently and when user gets removed via handleDelete in UserCard component - yes, you remove this user "physically" from your storage via这些组件中的每一个都独立加载“用户”,并且当用户通过 UserCard 组件中的 handleDelete 删除时 - 是的,您通过“物理”从存储中删除该用户

UserAPI.deleteUser

But, on the front-end you execute "refreshUserList":但是,在前端执行“refreshUserList”:

const deleteUser = (id) => {
        UserAPI.deleteUser(id)
            .then(() => {
                notifyUserDeleted()
                refreshUserList()
            })
            .catch(error => {
                console.error(error);
            });
    };

only in scope of current UserCard component and it refreshes "users" collection only in scope of UserCard component仅在当前 UserCard 组件的 scope 中,它仅在 UserCard 组件的 scope 中刷新“用户”集合

So parent Adminpage component does not have idea that one of users was removed and user list does not get updated所以父 Adminpage 组件不知道其中一个用户已被删除并且用户列表没有得到更新


The most easy "working" solution here is not to call useUsers() hook multiple times and pass handleDelete from Adminpage component to each UserCard component directly (as input parameter):这里最简单的“工作”解决方案是不要多次调用 useUsers() 挂钩并将 handleDelete 从 Adminpage 组件直接传递到每个 UserCard 组件(作为输入参数):

    function Adminpage() {
        const { users, refreshUserList, handleDelete } = useUsers();
    
        return (
            <div className="container" style={{ display: "flex" }}>
                <UserCardList handleDelete={handleDelete} users={users} refreshUserList={refreshUserList} />
                <InputAdmin refreshUserList={refreshUserList} />
            </div>
        );
    }

export default Adminpage;


function UserCardList(props) {
    return (
        <div className="container">
            <h4 style={{ margin: "3% 0 2% 0" }}>User list:</h4>
            <Bootstrap.Row>
                {
                    props.users.map(user =>
                        <UserCard key={user.id} handleDelete ={props.handleDelete} users={user} refreshUserList={props.refreshUserList} />
                    )
                }
            </Bootstrap.Row>
        </div>
    )
}

export default UserCardList;


function UserCard(props) {
    // do not call "useUsers" here !!!  instead accept needed callbacks as input parameters

    return (
        <Bootstrap.Col className="col-lg-4 col-12">
......... this code remains the same ..........

This technique would fix all back-end / front-end issues you face with minimal effort, but this fix has some disadvantages anyway:这种技术将以最小的努力解决您面临的所有后端/前端问题,但无论如何,这种修复有一些缺点:

  • you're able to use "useUsers" hook only once in component tree.您只能在组件树中使用一次“useUsers”挂钩。 And you have to remember this rule and always be careful with it你必须记住这条规则并时刻小心
  • you have to pass methods through all components in your component tree, like I did it with handleDelete above (Adminpage -> UserCardList -> UserCard).您必须通过组件树中的所有组件传递方法,就像我在上面使用 handleDelete 所做的那样(Adminpage -> UserCardList -> UserCard)。 For your component tree it's not too complex, but for larger component hierarchies it could become "a hell".对于您的组件树来说,它并不太复杂,但是对于更大的组件层次结构,它可能会变成“地狱”。 Just imagine that you do it for 5 callbacks through hierarchy of 10 components...想象一下,您通过 10 个组件的层次结构为 5 个回调执行此操作...

The first trouble (only single hook usage) could be fixed only if you'll connect your "useUsers" usages in some way.只有以某种方式连接“useUsers”用法,才能解决第一个问题(仅使用单钩子)。 Probably, you could utilize some rxjs-like approach with subscribers/notifications inside useUsers, share same "users" collection between them, and whenever something happens inside one of "useUsers" - notify others, etc... Other possible solution here - utilize some application-wide storage, like REDUX and store users there.可能,您可以在 useUsers 中使用一些类似 rxjs 的订阅者/通知方法,在它们之间共享相同的“用户”集合,并且每当“useUsers”之一发生某些事情时 - 通知其他人等......这里的其他可能解决方案 - 利用一些应用程序范围的存储,如 REDUX 并将用户存储在那里。 Anyway, that's definitely much complicated issue then one you already face and that's "better" to skip it if no urgency无论如何,这绝对是一个比你已经面临的问题复杂得多的问题,如果不紧迫的话,跳过它是“更好的”


To fix second issue (passing callback as input parameter through every component in hierarchy) React's useContext() hook could be handy https://beta.reactjs.org/reference/react/useContext要解决第二个问题(将回调作为输入参数传递给层次结构中的每个组件),React 的 useContext() 挂钩可能很方便https://beta.reactjs.org/reference/react/useContext

useEffect(() => { refreshUserList(); }, [users])

The 2nd parameter [users] tells useEffect when to fire. 第二个参数[users]告诉 useEffect 何时触发。 So you are actually refreshing the users when the users have changed, which then means never because the users change when they are refreshed.因此,您实际上是在用户更改时刷新用户,这意味着永远不会,因为用户在刷新时会更改。

The solution that Uladzimir presented worked. Uladzimir 提出的解决方案奏效了。 I had to pass everything from the adminpage into the components like this:我必须将管理页面中的所有内容传递到组件中,如下所示:

function Adminpage() {

    const { users, handleDelete, handleRegisterAdmin,
        adminUsername,
        setAdminUsername,
        adminPassword,
        showAdminModal,
        setShowAdminModal,
        setAdminPassword,
        showModal,
        setShowModal,
    } = useUsers()

    return (
        <div className="container" style={{ display: "flex" }}>
            <UserCardList users={users} handleDelete={handleDelete} showModal={showModal} setShowModal={setShowModal} />
            <InputAdmin handleRegisterAdmin={handleRegisterAdmin} adminUsername={adminUsername}
                setAdminUsername={setAdminUsername} adminPassword={adminPassword} setAdminPassword={setAdminPassword}
                showAdminModal={showAdminModal} setShowAdminModal={setShowAdminModal} />
        </div>
    );
}

export default Adminpage;

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM