React custom hook re-rendering - javascript

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.
I tried adding users to my useEffect in the hook but that didn't fix my problem.
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.
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.
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.
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>
...
<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.

useEffect(() => { refreshUserList(); }, [users])
The 2nd parameter [users] tells useEffect when to fire. 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 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)
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:
useEffect(() => {
refreshUserList(); // UserAPI.getUsers()
}, []);
Now imagine, you call this hook in Adminpage component - users get loaded from the back-end. When users are loaded - you render UserCardList with loaded users. And UserCardList renders UserCard component for each loaded user. 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 :(
You see the trouble, you load 20 users, and for each of these users you load all these users again. if you have 20 users - it would be 21 call on your back-end. 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
To prove this assumption please open network tab in your browser and just reload the page. 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
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
UserAPI.deleteUser
But, on the front-end you execute "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
So parent Adminpage component does not have idea that one of users was removed and user list does not get updated
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):
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. 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). 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...
The first trouble (only single hook usage) could be fixed only if you'll connect your "useUsers" usages in some way. 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. 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

The solution that Uladzimir presented worked. 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;

Related

Supabase onAuthStateChanged - How do I properly wait for the request to finish prevent flickering with useEffect?

Everything auth-wise is working fine. I even have a loading state setup so that the loader shows until the state is changed, but I still get this flickering on reload. This flickering only happens with Supabase. I was using the Firebase version before and it worked perfectly with my code.
Here is a video for reference: https://imgur.com/a/5hywXj5
Edit: Updated code to current version
export default function Navigation() {
const { user, setUser } = useContext(AuthenticatedUserContext);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const session = supabase.auth.session();
setUser(session?.user ?? null);
const { data: listener } = supabase.auth.onAuthStateChange((_: any, session: any) => {
setUser(session?.user ?? null);
});
setIsLoading(false);
return () => {
listener?.unsubscribe();
};
}, []);
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator color={Theme.colors.purple} size="large" />
</View>
);
}
return (
<NavigationContainer linking={LinkingConfiguration}>{user ? <AppStack /> : <AuthStack />}</NavigationContainer>
);
}
To recap for others, onAuthStateChange will not execute on first page load so you are triggering it using the getUserAuthStatus async function. However session() function is not async and will immediately return a result of null if there is no user session, or return a session that has been stored in localStorage.
In this case the result of the getUserAuthStatus will always return null. Then onAuthStateChange will trigger with the SIGNED_IN event and a session which will then set the user.
Furthermore the onAuthStateChange function should be registered before you perform the session step so as to capture any events triggered. In the current form an event may be triggered directly after the session() call but before the handler is registered.
So to recap the rendering steps will be:
Step 1
isLoading: true
user: null
Step 2
isLoading: false
user: null
Step 3
isLoading false
user: {...}
So far as I can tell, using session directly without thinking it's async will do the trick.
Ok, Supabase has released some updates since I first asked this question. Here is how I am now able to stop flickering when loading the application.
First, we need to set up our AuthContext for our application. Be sure to wrap your App.tsx with the <AuthContextProvider>.
AuthContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import { Session, User } from '#supabase/supabase-js';
import { supabase } from '../config/supabase';
export const AuthContext = createContext<{ user: User | null; session: Session | null }>({
user: null,
session: null,
});
export const AuthContextProvider = (props: any) => {
const [userSession, setUserSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setUserSession(session);
setUser(session?.user ?? null);
});
const { data: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {
console.log(`Supabase auth event: ${event}`);
setUserSession(session);
setUser(session?.user ?? null);
});
return () => {
authListener.subscription;
};
}, []);
const value = {
userSession,
user,
};
return <AuthContext.Provider value={value} {...props} />;
};
export const useUser = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useUser must be used within a AuthContextProvider.');
}
return context;
};
Now, if you're using React Navigation like me we need to check if we have a valid user to send them to the logged-in home screen. Here's how I do it.
Navigation.tsx
export default function Navigation() {
const { user } = useUser();
return (
<NavigationContainer linking={LinkingConfiguration}>
{user ? <AppStackNavigator /> : <AuthStackNavigator />}
</NavigationContainer>
);
}

What can I use in functional components to have same behavior as componentDidMount?

My UI was working fine until it was using a class component. Now I am refactoring it to a functional component.
I have to load my UI based on the data I receive from an API handler. My UI will reflect the state of the camera which is present inside a room. Every time the camera is turned on or off from the room, I should receive the new state from the API apiToGetCameraState.
I want the console.log present inside the registerVideoStateUpdateHandlerWrapper to print both on UI load for the first time and also to load every time the video state is changed in the room. However, it doesn't work when the UI is loaded for the first time.
This is how my component looks like:
const Home: React.FunctionComponent<{}> = React.memo(() => {
const [video, setToggleVideo] = React.useState(true);
const registerVideoStateUpdateHandlerWrapper = React.useCallback(() => {
apiToGetCameraState(
(videoState: boolean) => {
// this log does not show up when the UI is loaded for the first time
console.log(
`Video value before updating the state: ${video} and new state is: ${videoState} `
);
setToggleVideo(videoState);
}
);
}, [video]);
React.useEffect(() => {
//this is getting called when the app loads
alert(`Inside use effect for Home component`);
registerVideoStateUpdateHandlerWrapper ();
}, [registerVideoStateUpdateHandlerWrapper ]);
return (
<Grid>
<Camera
isVideoOn={video}
/>
</Grid>
);
});
This was working fine when my code was in class component. This is how the class component looked like.
class Home extends Component {
registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
console.log(`ToggleVideo value before updating the state: ${this.state.toggleCamera} and new state is: ${videoState}`);
this.setStateWrapper(videoState.toString());
})
}
setStateWrapper = (toggleCameraUpdated) => {
console.log("Inside setStateWrapper with toggleCameraUpdated:" + toggleCameraUpdated);
this.setState({
toggleCamera: (toggleCameraUpdated === "true" ) ? "on" : "off",
});
}
constructor(props) {
super(props);
this.state = {
toggleCamera: false,
};
}
componentDidMount() {
console.log(`Inside componentDidMount with toggleCamera: ${this.state.toggleCamera}`)
this.registerVideoStateUpdateHandlerWrapper ();
}
render() {
return (
<div>
<Grid>
<Camera isVideoOn={this.state.toggleCamera} />
</Grid>
);
}
}
What all did I try?
I tried removing the useCallback in the registerVideoStateUpdateHandlerWrapper function and also the dependency array from React.useEffect and registerVideoStateUpdateHandlerWrapper. It behaved the same
I tried updating the React.useEffect to have the code of registerVideoStateUpdateHandlerWrapper in it but still no success.
Move registerVideoStateUpdateHandlerWrapper() inside the useEffect() callback like this. If you want to log the previous state when the state changes, you should use a functional update to avoid capturing the previous state through the closure:
const Home = () => {
const [video, setVideo] = useState(false);
useEffect(() => {
console.log('Inside useEffect (componentDidMount)');
const registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
setVideo((prevVideo) => {
console.log(`Video value before updating the state: ${prevVideo} and new state is: ${videoState}`);
return videoState;
});
});
};
registerVideoStateUpdateHandlerWrapper();
}, []);
return (
<Grid>
<Camera isVideoOn={video} />
</Grid>
);
};
When you no longer actually need to log the previous state, you should simplify registerVideoStateUpdateHandlerWrapper() to:
const registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
setVideo(videoState);
});
};
import React from 'react'
const Home = () => {
const [video, setVideo] = useState(null);
//default video is null, when first load video will change to boolean, when the Camera component will rerender
const registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
setVideo(videoState);
});
};
useEffect(() => {
registerVideoStateUpdateHandlerWrapper();
}, []);
return (
<Grid>
<Camera isVideoOn={video} />
</Grid>
);
};
export default Home
componentDidMount() === useEffect()
'useEffect' => import from 'react'
// componentDidMount()
useEffect(() => {
// Implement your code here
}, [])
// componentDidUpdate()
useEffect(() => {
// Implement your code here
}, [ update based on the props, state in here if you mention ])
e.g:
const [loggedIn, setLoggedIn] = useState(false);
useEffect(() => {
// Implement the code here
}, [ loggedIn ]);
the above code will act as equivalent to the componentDidUpdate based on 'loggedIn' state

Redux-triggered function not registering component current state

TL;DR
When a store change triggers a component function, the current component state is ignored/reset, not letting me use its state data to feed the triggered function.
Full Description
This react-native app has a button located in a heading Appbar stack navigator, which must trigger a function that the currently focused Screen has.
The thing is that this screen is very deep within the navigation scheme, thus I decided to use Redux to directly notify the screen that the button has been pressed.
This also means that every time that this button is pressed and a store slice gets dispatched, I can trigger any function only depending on the Screen implementation.
If i use the very same function from a button within the component it works perfectly. However if I call the same function from the redux store change i get this log:
Console Behavior
# component loaded
false
# started writing, this is the component state
h
he
hel
hell
hello
#header button 'create' state change detected
true
#content as viewed by the onPressPublish function
content: ""
Error 400 - Cannot save empty content
#store reset for further use
false
Appbar
export const AppBarStackNavigator = (props) => {
const { toggle } = useSelector(toolbarSelector);
const handleCreatePress = () => {
dispatch(setCreate({ pressed: true }));
}
return (
<Appbar.Header
style={{ backgroundColor: theme.colors.background, elevation: 0 }}
>
<Button
icon="seed"
mode="contained"
// disabled={!contentProps.valid}
onPress={handleCreatePress}
labelStyle={{ color: 'white' }}
style={{
width: 115,
borderRadius: 50,
alignSelf: "flex-end"
}}>
Sembrar
</Button>
</Appbar.Header>
);
}
Store
import { createSlice } from "#reduxjs/toolkit";
export const toolbarSlice = createSlice({
name: 'toolbar',
initialState: {
create: false
},
reducers: {
setCreate(state, action) {
state.create = action.payload.pressed;
}
}
})
export const { setCreate } = toolbarSlice.actions;
export const toolbarSelector = state => state.toolbar
export default toolbarSlice.reducer;
The navigationally-deep component
import { toolbarSelector, setCreate } from '../store/toolbar';
import { useSelector } from 'react-redux';
// import { useFocusEffect, TabActions, useNavigation } from '#react-navigation/native';
export const DeepComponent = (props) => {
const theme = useTheme();
const { create } = useSelector(toolbarSelector);
return (
<ChildComponent {...props} create={create} setCreate={setCreate} style={{ backgroundColor: theme.colors.background }} />
);
};
Its child (where the function is)
import { useDispatch, useSelector } from 'react-redux';
export const ChildComponent = (props) => {
const dispatch = useDispatch();
const [content, setContent] = useState(''));
let payload = {};
const onPressPublish = async () => {
try {
console.log(payload);
payload = {
...payload,
content,
// images <- other component states
}
console.log(payload);
const seed = await api.writeOne(payload);
} catch (error) {
console.log(error);
Alert.alert('Could not publish :(', error.message);
}
navigation.goBack();
}
useEffect(() => {
console.log(props.create)
console.log(content)
if (props.create) {
console.log(content)
onPressPublish();
}
return () => {
dispatch(props.setCreate({ pressed: false }));
};
}, [props.create])
const onTextChange = (value, props) => {
// ...
setContent(value);
// ...
}
return (
<TextInput
mode='flat'
placeholder={inputPlaceholder}
multiline
onChangeText={text => onTextChange(text, props)}
keyboardShouldPersistTaps={true}
autoFocus
clearButtonMode='while-editing'>
<ParsedContent content={content} />
</TextInput>
<Button
disabled={!contentProps.valid}
onPress={onPressPublish}>
{buttonText}
</Button>
)
}
Here are some suggestions to change the code, you still have not provided any code in your question that would make sense (like the payload variable) but this may give you an idea where to go.
When you create an app with create-react-app you should have a linter that tells you when you have missing dependencies in hooks, you should not ignore these warnings:
//create onPressPublish when component mounts
const onPressPublish = useCallback(async (content) => {
try {
//removed payload as it makes no sense to create it
// and never use it anywhere
//removed assigning to seed becasue it is never used anywhere
await api.writeOne({ content });
} catch (error) {
console.log(error);
Alert.alert('Could not publish :(', error.message);
}
navigation.goBack();
}, []);
const { create, setCreate } = props;
useEffect(() => {
if (create) {
//passing content
onPressPublish(content);
}
return () => {
dispatch(setCreate({ pressed: false }));
};
}, [
//correct dependencies without linter warnings
content,
dispatch,
onPressPublish,
create,
setCreate,
]);

Using React Context as part of useEffect dependency array for simple toast notification system

I'm building a simple toast notification system using React Context. Here is a link to a simplified but fully working example which shows the problem https://codesandbox.io/s/currying-dust-kw00n.
My page component is wrapped in a HOC to give me the ability to add, remove and removeAll toasts programatically inside of this page. The demo has a button to add a toast notification and a button to change the activeStep (imagine this is a multi-step form). When the activeStep is changed I want all toasts to be removed.
Initially I did this using the following...
useEffect(() => {
toastManager.removeAll();
}, [activeStep]);
...this worked as I expected, but there is a react-hooks/exhaustive-deps ESLint warning because toastManager is not in the dependency array. Adding toastManager to the array resulted in the toasts being removed as soon as they were added.
I thought I could have fixed that using useCallback...
const stableToastManager = useCallback(toastManager, []);
useEffect(() => {
stableToastManager.removeAll();
}, [activeStep, stableToastManager]);
...however, not only does this not work but I would rather fix the issue at the source so I don't need to do this every time I want this kind of functionality, as it is likely to be used in many places.
This is where I am stuck. I'm unsure as to how to change my Context so that I don't need add additional logic in the components that are being wrapped by the HOC.
export const ToastProvider = ({ children }) => {
const [toasts, setToasts] = useState([]);
const add = (content, options) => {
// We use the content as the id as it prevents the same toast
// being added multiple times
const toast = { content, id: content, ...options };
setToasts([...toasts, toast]);
};
const remove = id => {
const newToasts = toasts.filter(t => t.id !== id);
setToasts(newToasts);
};
const removeAll = () => {
if (toasts.length > 0) {
setToasts([]);
}
};
return (
<ToastContext.Provider value={{ add, remove, removeAll }}>
{children}
<div
style={{
position: `fixed`,
top: `10px`,
right: `10px`,
display: `flex`,
flexDirection: `column`
}}
>
{toasts.map(({ content, id, ...rest }) => {
return (
<button onClick={() => remove(id)} {...rest}>
{content}
</button>
);
})}
</div>
</ToastContext.Provider>
);
};
export const withToastManager = Component => props => {
return (
<ToastContext.Consumer>
{context => {
return <Component toastManager={context} {...props} />;
}}
</ToastContext.Consumer>
);
};
If you want to "Fix it from the core", you need to fix ToastProvider:
const add = useCallback((content, options) => {
const toast = { content, id: content, ...options };
setToasts(pToasts => [...pToasts, toast]);
}, []);
const remove = useCallback(id => {
setToasts(p => p.filter(t => t.id !== id));
}, []);
const removeAll = useCallback(() => {
setToasts(p => (p.length > 0 ? [] : p));
}, []);
const store = useMemo(() => ({ add, remove, removeAll }), [
add,
remove,
removeAll
]);
Then, the useEffect will work as expected, as the problem was that you re-initialized the ToastProvider functionality on every render when it needs to be a singleton.
useEffect(() => {
toastManager.removeAll();
}, [activeStep, toastManager]);
Moreover, I would recommend to add a custom hook feature as the default use case, and providing wrapper only for class components.
In other words, do not use wrapper (withToastManager) on functional components, use it for classes, as it is considered an anti-pattern, you got useContext for it, so your library should expose it.
// # toastContext.js
export const useToast = () => {
const context = useContext(ToastContext);
return context;
};
// # page.js
import React, { useState, useEffect } from 'react';
import { useToast } from './toastContext';
const Page = () => {
const [activeStep, setActiveStep] = useState(1);
const { removeAll, add } = useToast();
useEffect(() => {
removeAll();
}, [activeStep, removeAll]);
return (
<div>
<h1>Page {activeStep}</h1>
<button
onClick={() => {
add(`Toast at ${Date.now()}!`);
}}
>
Add Toast
</button>
<button
onClick={() => {
setActiveStep(activeStep + 1);
}}
>
Change Step
</button>
</div>
);
};
export default Page;

How to clean up the useEffect function?

Im new to react and started using hooks but I'm receiving warning on my console
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function
I tried to fix it by defining a variable to check if its unmount or not(saw on another stackoverflow question. But it did not work.
here is my code
const Page1 = (props) => {
const [averageAPICallsPerDay, setAverageAPICallsPerDay] = useState(false);
const [org, setOrg] = useContext(OrgContext);
const [catalog, setCatalog] = useContext(CatalogContext);
const [didMount, setDidMount] = useState(false);
function getAverageAPICallPerDay() {
axios.get(`${ANALYTICS_BASEPATH}/${org}/${catalog}/analytics/calls/average`)
.then(res => setAverageAPICallsPerDay(res.data.data.averageCalls))
.catch(err => setAverageAPICallsPerDay('No records found'))
}
useEffect(() => {
setDidMount(true);
getAverageAPICallPerDay()
return(didMount)
}, [catalog, org])
return(
<Container>
<SubContainer>
<Row>
<Col><Card cardTitle="Total API Calls" totalCalls={averageAPICallsPerDay}/></Col>
</Row>
</SubContainer>
</Container>
);
};
I think the issue here might be that you are updating your state from your then or catch function once your component has been unmounted. I think you can define at the top of your effect a isCancelled variable with false, and from your effect return a function that set's it to true, and check that function in your then function.. something like this.
useEffect(() => {
let isCanceled = false
function getAverageAPICallPerDay() {
axios.get(`${ANALYTICS_BASEPATH}/${org}/${catalog}/analytics/calls/average`)
.then(res => {
if (!isCanceled) {
setAverageAPICallsPerDay(res.data.data.averageCalls)
}
})
.catch(err => {
if (!isCanceled) {
setAverageAPICallsPerDay('No records found')
}
})
}
setDidMount(true);
getAverageAPICallPerDay()
return () => {
isCanceled = true
}
}, [catalog, org])

Categories