My HomeScreen component contains an API call to get the current user. Is there a way to have it make an API call ONLY if the user has changed. As of now, if I move away from the screen and then come back it makes an API call and displays the loader which I think is not perfect user experience not to mention that the API request is completely redundant since the user has not changed.
useEffect(() => {
dispatch(userRequest({ userId, userRole }))
}, [dispatch, userId, userRole])
I also tried to use reselect to get the userId and the userRole from Redux and wrapper my HomeScreen component in React.memo but it seems to get rerendered every time
Here is how I get the data from Redux
const userId = useSelector(selectUserId)
const userRole = useSelector(selectUserRole)
My selectors:
const loginInfoSelector = (state: AppStateType) => state.loginInfo
export const selectUserId = createSelector(
loginInfoSelector,
(loginInfo) => loginInfo.id
)
export const selectUserRole = createSelector(
loginInfoSelector,
(loginInfo) => loginInfo.role
)
I would try to remove dispatch from dependency array as:
useEffect(() => {
dispatch(userRequest({ userId, userRole }))
}, [userId, userRole])
In this way it's only triggered once userId or userRole are changing.
Related
I can load my data but only after I refresh the page. Until then, it shows the data from the previous item I clicked on. It's behaving like a cache would.
Here is my mediaSlice
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import axios from "axios";
const KEY = process.env.REACT_APP_API_KEY
const BASE_URL = process.env.REACT_APP_BASE_URL
const HBO_SINGLE_MEDIA_API = `${BASE_URL}/titlestest`
const initialState = {
media:{},
status: 'idle', //'idle', 'loading', 'succeeded', 'failed'
error:null
}
export const fetchSingleMediaTitle = createAsyncThunk(
'media/fetchSingleMediaTitle',
async (id) => {
const response = await axios.get(
HBO_SINGLE_MEDIA_API,
{
headers: {
'Content-Type': 'application/json',
'X-API-KEY': KEY,
},
params: {
titleId: id,
}
}
)
return response.data.Item;
}
)
const mediaSlice = createSlice({
name: 'media',
initialState,
reducers:{},
extraReducers: {
[fetchSingleMediaTitle.pending]: () => {
console.log("Pending");
},
[fetchSingleMediaTitle.fulfilled]: (state, { payload }) => {
state.status = 'succeeded'
state.media = payload
},
[fetchSingleMediaTitle.rejected]: () => {
console.log("Rejected");
},
}
})
// SELECTORS
export const selectSingleMedia = (state) => state.media.media;
export const getMediaStatus = (state) => state.media.status;
export default mediaSlice.reducer
And then the Media Component has what you would expect
const [media, setMedia] = useState([]);
const {id} = useParams();
const dispatch = useDispatch();
const singlemedia = useSelector((state) => selectSingleMedia(state, id))
const mediaStatus = useSelector(getMediaStatus)
useEffect(() => {
if (mediaStatus === 'idle') {
dispatch(fetchSingleMediaTitle(id)) //yes, it's imported
}
setMedia(singlemedia);
//it returns the array with the data but not the current one
console.log("singlemedia: ", singlemedia);
return () => { };
// Someone suggested removing the dependencies below but then it doesn't load anything.
}, [id, singlemedia, mediaStatus, dispatch, media_data])
I am at a loss here because as I understand it, the useEffect is supposed to fire onload and give me the current data. The ID is correct in the params but the state is not mutating.
Thanks in advance
EDIT
For reference, here is the console log. Those console logs are in the useEffect. The API is slightly slower than the useEffect and the render is happening before the data is ready (race condition) or at least that's what I think it's happening here that's why it loads empty and then it loads again. But the confusing part is that ALL of that happens on a page refresh only. On a normal load the state is not empty is loaded and in time for the UI to render it (no race condition), only, it's loaded with the old data from a previous state shape
Here is the redux dev tools
Your selector takes one argument, but you're passing it two. Change it to:
const singlemedia = useSelector(selectSingleMedia);
Second, your singlemedia is the state. There's no need to setMedia(singlemedia);.
Your useEffect should look like:
useEffect(() => {
if (mediaStatus === 'idle') {
dispatch(fetchSingleMediaTitle(id));
}
}, [dispatch, id, mediaStatus]);
Also, you should look into RTK Query, which would replace createAsyncThunk: https://redux-toolkit.js.org/tutorials/rtk-query
Edit per our discussion:
useEffect(() => {
dispatch(fetchSingleMediaTitle(id));
}, [dispatch, id]);
The problem is very simple, just remove the media internal state variable:
const [media, setMedia] = useState([]);
As what you are doing now is that you send the async request here:
if (mediaStatus === 'idle') {
dispatch(fetchSingleMediaTitle(id)) //yes, it's imported
}
And before the backend responds you read the store and set it in the internal state:
setMedia(singlemedia); // This is old data now, as we are waiting for the backend
If you wish to display the store state just use singlemedia in the render method.
If you wish to have some temporary state that mimics a backend response again use singlemedia and implement the temporary state in the redux store.
PS. the useEffect should depend only on id and useDispatch
I am using redux in a project and I want to make a useSelector that would check to see if the values in the redux state are the default values if not it will do a request to the database and update the the state I feel like it is quite complicated though and I am having a hard time getting my head around how I need to do this.
I need to do this because sometimes the correct state is not loaded in the state I am considering just doing a check every time I use useSelector to check if the values are the default values then fetch from the database but I would much prefer to write it a way that would allow to be handled within the redux selector but I can't really grasp I how I need to do it.
const info = useSelector(getInfo)
Ideally I would like the info to be handled when I fetch here
import { SET_USER_DETAILS } from "../constants/userConstants";
const intialState = {
user: { },
};
const userReducer = (state = intialState, action: any) => {
switch (action.type) {
case SET_USER_DETAILS:
return { ...state, user: action.payload };
default:
return state;
}
};
here is what my current reducer looks like what would be the best way to do this as I am finding it a little bit difficult to follow the documentation on the redux website.
You can use redux-thunk. https://redux.js.org/usage/writing-logic-thunks
then your thunk could look something like that:
const thunkFunction = (dispatch, getState) => {
// logic here that can dispatch actions or read state
const currentState = getState() as typeof initialState;
// check if state is default state
if (JSON.stringify(initialState) === JSON.stringify(currentState)) {
fetch(url).then(data => {
dispatch({type: SET_USER_DETAILS, payload: data})
})
}
}
You need first to fetch data in react component:
const MyComponent = () => {
// while fetch is fetching, data will be default state,
// and when fetch is done, that component will automatically
// rerender with new data
const data = useSelector(getInfo);
const dispatch = useDispatch();
useEffect(() => {
dispatch(thunkFunction)
},[])
return <code>{JSON.stringify(data, null, 2)}</code>
}
I did not test it so may require some changes
but in general concept is like this
I'm building a headless eCommerce site using React/Next and have a [product].js dynamic route which is used to generate all product pages, using getStaticPaths() and getStaticProps() which generates the pages fine.
I'm using useState hook within [product].js to manage a number input (for quantity) and a couple of other things.
The first product page loaded works fine, but when I go to other product pages, they use the same state from the first product.
Is there a way to have the state NOT persist between route changes?
Through some digging, I found that this is an issue with next and is in their backlog. It essentially stems from the fact that the component doesn't have a key. This means switching between routes on the same dynamic route doesn't register correctly and causes the component to use stale state.
A possible solution I found was this:
export async function getStaticProps({params}) {
const props = await getData(params);
// key is needed here
props.key = data.id;
return {
props: props
}
}
This is my implementation which doesn't work for me:
export default function ProductPage(props) {
// this state doesn't reset between dynaic route changes
const [quantity, setQuantity] = useState(1)
return(
...
)
}
export async function getStaticProps({ params }) {
const slug = params.product
const props = await client.query({
query: singleProductQuery,
variables: { id: slug }
})
props.key = props.data.product.slug
return {
props: props
}
}
I tried wrapping the contents within another component and adding a key to that, like so:
return(
<OuterComponent key={props.id}>
// components within here, that have their own state, now work
</OuterComponent>
)
Since this new keyed component is only in the return statement and does not encapsulate the state hook, it does not work. This does reset the state however, for any components found within wrapped component.
You can use useEffect hook and useRouter hook at dynamic router to reset the state.
import {useState, useEffect} from 'react'
import {useRouter} from 'next/router'
const ProductPage = (props) => {
const [state, setState] = useState(someState)
const dynamicRoute = useRouter().asPath
useEffect(() => {
setState(resetState) // When the dynamic route change reset the state
}, [dynamicRoute])
//Some other logic
return (
......
)
}
It seems that you've encountered the same issue thread that I've found:
https://github.com/vercel/next.js/issues/9992
It seems from what I've read that to fix your case, all you need to do is change your getStaticProps to return an object with a unique key:
export async function getStaticProps({ params }) {
const slug = params.product
const props = await client.query({
query: singleProductQuery,
variables: { id: slug }
});
return {
props: props,
key: slug
}
}
What you've been doing previously is passing a key to the props object instead of root return object for getStaticProps
You can use useEffect hook to reset state
export default function ProductPage(props) {
// this state doesn't reset between dynaic route changes
const [quantity, setQuantity] = useState(1)
useEffect(() => {
setQuantity(props.quantity) // <-- this props comes from getStaticProps
}, [props]) // <--- useEffect will keep tracking changing props
return(
...
)
}
So when your props changes - your state updates.
I want save array data using react useEffect. Follow Example with class:
async componentDidMount() {
const users = await AsyncStorage.getItem('users');
if (users) {
this.setState({ users: JSON.parse(users) });
}
}
componentDidUpdate(_, prevState) {
const { users } = this.state;
if (prevState.users !== users) {
AsyncStorage.setItem('users', JSON.stringify(users));
}
}
how to implement the logic with React Hooks?
For componentDidMount logic you can use useEffect hook:
useEffect(() => {
const asyncFetch = async () => {
const users = await AsyncStorage.getItem("users");
if (users) {
// setter from useState
setUsers(JSON.parse(users));
}
};
asyncFetch();
}, []);
For componentDidMount use useEffect with dep array and useRef reference.
const prevUsers = useRef();
useEffect(() => {
const prevUsers = prevUsers.current;
// Some equal check function
if (!areEqual(prevUsers, users)) {
AsyncStorage.setItem("users", JSON.stringify(users));
}
prevUsers.current = users;
}, [users]);
Notice that in your current code, prevState.users !== users is always truley, you comparing two objects and in JS {} !== {} always results true.
You can try like below and you can use hooks in functional based component not class based component
//state declaration similar to class based component
const [usersdata,setUsers] = useState([]);
const users = await JSON.parse(AsyncStorage.getItem('users'));
//whenever the value of users changes useEffect will reset the value of users in state useEffect handle the lifecycle in function based component
useEffect(()=>{
if(users){
setUsers(JSON.parse(users));
}
},[users])
For hooks the logic changes slightly, you would have to "hook" your effect with a state in order to update the component, so the component would update (componentDidUpdate) when the hooked state has been updated, you can obviously hook multiple states.
If you choose to not hook any state, the effect would execute only at the mounting of the component just like (componentDidMount())
I don't see the logic that makes you decide when to update the user state since you always get it from the storage, so I will assume that you have some kind of a trigger that makes you verify if the users value has changed in the storage.
so you can refactor your code like this:
const [users, setUsers] = useState([]);
const [userHasChanged, setUserHasChanged] = useState(false);
usEffect(async () => {
// comparing the old users with the new users is not useful since you always fetch the users from the storage, so the optimal is to always set the new array/ object to users, this way you avoid comparing the two objects which is a bit costly.
const newUsers = await AsyncStorage.getItem("users");
setUsers(JSON.parse(newUsers));
setUserHasChanged(false);
}, [userHasChanged])
// some code that triggers userHasChanged, you use setUserHasChaned(true)
I would like to use a Context.Provider value to handle both mutating and dispatching similar changes. I have read about React-Apollo's onComplete method, but I'm not sure which approach will cover more cases where I need to both mutate and dispatch state. Here's what I have:
const CartContext = React.createContext<{
state: State
dispatch: Dispatch<AnyAction>
cartApi: any
}>({ state: initialState, dispatch: () => null, cartApi: mutateUserProductsAndUpdateCart })
function CartProvider({ children }: { children?: React.ReactNode }) {
const [state, dispatch] = useReducer<Reducer<State, AnyAction>>(reducer, initialState)
// feel like i need to do something with the hook here to avoid invariant violation side effects
const [updateUserProducts] = useUpdateUserProducts()
return (
<CartContext.Provider value={{ state, dispatch, cartApi: mutateUserProductsAndUpdateCart}}>
{children}
</CartContext.Provider>
)
}
export const useCartState = () => useContext(CartContext)
And here's what I would like to do with my mutateUserProductsAndUpdateCart:
const mutateUserProductsAndUpdateCart = async (_mutation: any, _mutationParams: any, _dispatchObject: AnyObject) => {
// apollo mutation
const updateUserProductsResult = await updateUserProducts(_mutationParams)
if (updateUserProductsResult.error) throw Error("wtf")
// useReducer dispatch
dispatch(_dispatchObject)
return
}
and here is how I would like to access this on another component:
const { cartApi } = useCartState()
const addProductToCart = async () => {
const result = await cartApi({
mutation,
mutationVariables,
dispatchObject})
}
I feel like this article is sort of the direction I should be taking, but I'm very lost on implementation here. Thanks for reading.
I'm not sure this directly answers your question, but have you considered just using Apollo Client? It looks like you are trying to do two things:
Save items added to the cart to the server
Update the cart locally in the cache
It seems like you could skip creating your own context altogether and just create a hook for mutating (saving your cart items) and then update your local cache for cart items. Something like this:
import gql from 'graphql-tag';
import useMutation from '#apollo/client';
export const mutation = gql`
mutation($items: [CartItem]!) {
saveCartItems(items: $items) {
id
_list_of_properties_for_cache_update_
}
}
`;
export const useSaveCartItems = mutationProps => {
const [saveCartItems, result] = useMutation(
mutation,
mutationProps
);
return [
items => {
saveCartItems({
update: (cache, { data }) => {
const query = getCartQuery; // Some query to get the cart items from the cache
const results = cache.readQuery({ query });
// We need to add new items only; existing items will auto-merge
// Get list of new items from results
const data = []; // List of new items; IMPLEMENT ME!!!
cache.writeQuery({ query, data });
},
variables: { items },
});
},
result,
];
};
Then in your useCartState hook you can just query the local cache for the items using the same query you used for the update and return that. By using the update function you can fix your local cache and anybody can access it from anywhere, just use the hook. I know that isn't exactly what you asked for, but I hope it helps.
Apollo client documentation on handling this may be found here.