React自定义钩子重新渲染



我正在学习react,现在我有一个包含用户和电影的react应用程序,无论何时登录,你都可以在其中查看有关电影的信息。如果你以管理员身份登录,你可以访问一个管理员页面,在那里你可以查看用户卡列表并注册另一个管理员。此卡片列表无需刷新即可更新。

由于我写的代码不是那么干净,所以我想加入自定义钩子。问题是,使用新的自定义挂钩,除了渲染之外,其他一切都很好。每当我删除用户或添加新管理员时,除非刷新页面,否则卡片列表不会更新。

我现在有一个自定义钩子useUsers,但我只将它用于输入字段和toast通知。我试着在钩子中将用户添加到我的useEffect中,但这并没有解决我的问题。

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

这是我的密码。

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;

问题

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

users添加到useEffect挂钩可能会导致渲染循环,因为refreshUserList最终会更新users状态。不要无条件地更新钩子的任何依赖项。

React钩子也不共享状态。您有两个组件,AdminpageUserCard,每个组件都使用useUsers钩子的单独实例,每个实例都有自己的状态。改变useUsers的一个实例中的状态不会影响useUsers的任何其他实例。

解决方案

将状态和逻辑从useUsers移动到单个React上下文提供程序,并允许useUsers钩子的所有实例访问单个上下文值。

示例:

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>
);
}

使用UsersProvider组件包装应用程序代码,以提供用户状态和回调。

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

现在,使用useUsers钩子在UsersProvider的子Reactree中呈现的所有组件都将访问和引用相同的状态和回调。

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

第二个参数[users]告诉useEffect何时激发。因此,当用户发生更改时,实际上是在刷新用户,这意味着永远不会,因为用户在刷新时会发生更改。

您在React中面临的问题非常常见:)每个钩子的使用都会创建它的"拥有";孤立的范围。这意味着每个useUsers()使用都会产生单独的用户集合(从useEffect内部的后端检索),并公开一组单独的用户操作特定方法(如handleDelete)


首先,在回答最初的问题之前,让我们揭示一下你可能很快就会面临的非常非常戏剧性的麻烦

就每个";useUsers";执行有自己的作用域,每次使用这个钩子时,都会执行以下代码:

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

现在想象一下,您在Adminpage组件中调用这个钩子——用户从后端加载。加载用户时,您将使用已加载的用户呈现UserCardList。UserCardList为每个加载的用户呈现UserCard组件。最后,每个UserCard组件调用";useUsers";您创建的钩子,每个钩子都调用上面的代码(useEffect->refreshUserList),因此每个UserCard组件都会从后端再次加载用户:(

你看到了问题,你加载了20个用户,然后为每个用户再次加载所有这些用户。如果你有20个用户,那么你的后端就有21个呼叫。如果你有100个用户,那将是101个后端调用,等等。后端的性能肯定会很快降级

为了证明这一假设,请在浏览器中打开网络选项卡,然后重新加载页面。你肯定会看到无穷无尽的加载用户请求序列。。。


这是关于后端的,但当用户被删除时,前端不工作的最初问题仍然是打开

情况如下:您使用";useUsers";在Adminpage组件中呈现用户列表,然后使用";useUsers";在每个UserCard组件中调用";handleDelete";无论何时需要

这些部件中的每一个都加载";用户";独立地,当用户通过UserCard组件中的handleDelete被删除时——是的,你删除了这个用户";物理上";通过从您的存储

UserAPI.deleteUser

但是,在前端执行";refreshUserList":

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

仅在当前UserCard组件的范围内;用户";仅在UserCard组件范围内收集

所以父Adminpage组件不知道其中一个用户被删除了,并且用户列表并没有得到更新的


;工作";这里的解决方案不是多次调用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 ..........

这种技术可以用最少的精力解决您面临的所有后端/前端问题,但无论如何,这种修复方法都有一些缺点:

  • 您可以使用";useUsers";在组件树中只挂起一次。你必须记住这条规则并时刻小心
  • 您必须通过组件树中的所有组件传递方法,就像我在上面的handleDelete中所做的那样(Adminpage->UserCardList->UserCard)。对于组件树来说,它并不太复杂,但对于更大的组件层次结构,它可能会变成";地狱";。想象一下,您通过10个组件的层次结构为5个回调执行此操作

第一个问题(仅使用单钩)只有在您连接";useUsers";以某种方式使用。也许,您可以使用一些类似rxjs的方法,在useUsers内部使用订阅者/通知,共享相同的";用户";它们之间的集合,以及每当";useUsers"-通知其他人,等等。其他可能的解决方案在这里-利用一些应用程序范围的存储,如REDUX,并将用户存储在那里。无论如何,这肯定是一个非常复杂的问题,然后你已经面临的问题,那就是";"更好";如果没有紧急情况则跳过


要解决第二个问题(将回调作为输入参数传递给层次结构中的每个组件),React的useContext()钩子可能很方便https://beta.reactjs.org/reference/react/useContext

Uladzimir提出的解决方案行之有效。我必须将所有内容从adminpage传递到如下组件中:

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;

最新更新