React useEffect not running on dependency change - javascript

What I am trying to do
I have a lobby that users can join. To persist the joined lobby on the client on a page refresh I decided to put the lobby that has been joined into the browser's session storage. Before it was just in a useState which doesn't persist through a page refresh.
Setting Session Storage is classified as a side effect as far as I know and should be handled in useEffect. The problem is when I set the lobby the useEffect that has the lobby as a dependency doesn't run.
Setting breakpoints shows that it doesn't run at all, but I can see that the joinedLobby has changed from undefined to an object (example : {success: "Successfully joined ...", payload : { id:"", ...}}).
The session store stays empty.
Code Sandbox
Sandbox
CSS is broken since I was using Emotion
Update
Fetching Data from the back end breaks the app. Making the data static made the app function like it should.
I have 0 ideas on why / how. The culprit seems to be play_index.jsx at line 165 const jsonResponse.
Setting the state that should update the useEffect
const { setJoinedLobby } = useContext(JoinedLobbyProviderContext);
const history = useHistory();
useEffect(() => {
if (joinState.result === undefined) return;
setJoinedLobby(joinState.result);
history.push('/lobby');
}, [joinState.result, history, setJoinedLobby]);
Provider inside router
<JoinedLobbyProviderContext.Provider
value={{ getJoinedLobby, setJoinedLobby }}>
<Route path='/play'>
<Play />
</Route>
<Route path='/lobby'>
<Lobby />
</Route>
</JoinedLobbyProviderContext.Provider>
The functions the provider takes
const [joinedLobby, setJoinedLobby] = useState(undefined);
useEffect(() => {
if (joinedLobby === undefined) return;
sessionStorage.setItem('joinedLobby', JSON.stringify(joinedLobby));
}, [joinedLobby]);
const getJoinedLobby = () => {
return JSON.parse(sessionStorage.getItem('joinedLobby'));
};
Edit : How joinState.result changes
const joinInit = {
errors: undefined,
loading: false,
result: undefined,
id: undefined,
};
const joinReducer = (state, action) => {
switch (action.type) {
case 'joinLobby': {
return { ...state, id: action.payload };
}
case 'loadingTrue':
return { ...state, loading: true };
case 'setResult':
return { ...state, loading: false, result: action.payload };
case 'setErrors':
return {
...state,
loading: false,
errors: action.payload,
};
case 'reset':
return joinInit;
default : {throw new Error('Didn't find action passed to reducer')}
}
};
const [joinState, joinStateDispatch] = useReducer(joinReducer, joinInit);
const passRef = useRef();
useEffect(() => {
const joinLobby = async () => {
joinStateDispatch({ type: 'loadingTrue' });
try {
const jsonResponse = await (
await fetch(`${BACKEND_URL}/play/joinLobby/${joinState.id}`, {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify({
password: passRef.current.value,
}),
})
).json();
joinStateDispatch({ type: 'setResult', payload: jsonResponse });
} catch (e) {
joinStateDispatch({ type: 'setErrors', payload: e });
}
};
if (joinState.id !== undefined) {
joinLobby();
}
}, [joinState.id, joinStateDispatch]);

Related

useFetch data is undefined

const { response, setResponse } = useResponseState();
const handleNext = () => {
if (
response.currentResponse !== undefined &&
response.responses!== undefined
) {
if (response.currentResponse< response.responses.length) {
setResponse({
currentResponse: response.currentResponse + 1,
responses: response.responses,
});
}
}
};
const responseID= response.responses![response.currentResponse!].id ?? 0;
const { data, error } = useFetch<ExampleType>(
`${process.env.REACT_APP_API_ENDPOINT}example/${exampleID}`
);
return error || !data ? (
<>error</>
) : (
<>success</>
Can anyone help me understand why when handleNext is called data is undefined. In the success part of the return there is a button with an onclick but I have tried to show only what you need to see. Can anyone see anything wrong here?
ResponseState is a context.
Endpoint returns something like:
{"id":1,"exampleProp2: "test"}
Hook:
import { useEffect, useReducer, useRef, useState } from 'react';
import State from './State';
type Cache<T> = { [url: string]: T };
// discriminated union type
type Action<T> =
| { type: 'loading' }
| { type: 'fetched'; payload: T }
| { type: 'error'; payload: Error };
function useFetch<T = unknown>(url?: string, options?: RequestInit): State<T> {
const cache = useRef<Cache<T>>({});
// Used to prevent state update if the component is unmounted
const cancelRequest = useRef<boolean>(false);
const initialState: State<T> = {
error: undefined,
data: undefined,
};
// Keep state logic separated
const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
switch (action.type) {
case 'loading':
return { ...initialState };
case 'fetched':
return { ...initialState, data: action.payload };
case 'error':
return { ...initialState, error: action.payload };
default:
return state;
}
};
const [state, dispatch] = useReducer(fetchReducer, initialState);
useEffect(() => {
// Do nothing if the url is not given
if (!url) return;
const fetchData = async () => {
dispatch({ type: 'loading' });
// If a cache exists for this url, return it
if (cache.current[url]) {
dispatch({ type: 'fetched', payload: cache.current[url] });
return;
}
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(response.statusText);
}
const data = (await response.json()) as T;
cache.current[url] = data;
if (cancelRequest.current) return;
dispatch({ type: 'fetched', payload: data });
} catch (error) {
if (cancelRequest.current) return;
dispatch({ type: 'error', payload: error as Error });
}
};
void fetchData();
// Use the cleanup function for avoiding a possibly...
// ...state update after the component was unmounted
return () => {
cancelRequest.current = true;
};
}, [url]);
return state;
}
export default useFetch;
This is the exact hook used. Is there anything still not working here?
I depends on what's the API contract of useFetch are you calling (how the hook is working and what are the expected return values). But generally fetching is an asynchronous operation which is done on the background. The data can be really undefined or null at the first phase because the request has not been sent or response has not been received.
Let's say the hook returns the following stuff - { data, error, loading, requested }. The return value could be following:
fetch not sent: { loading: false, requested: false } (that's not probable in your case)
fetch sent, response not received: { loading: true, requested: true }
success response received: { loading: false, requested: true, data: {} }
failure response received: { loading: false, requested: true, error: {} }
As you can can see, there is just one state in which the data are expected to be available. This just a theoretical elaboration because you have not specified your useFetch hook enough.
For example, you could be using the hook from use-http. If you check the documentation then you should notice that they suggest the initialize data by own value to avoid undefined. In your case, it would be something like this:
const url = `${process.env.REACT_APP_API_ENDPOINT}example/${exampleID}`
const { data = {}, error, loading } = useFetch < ExampleType > (url);
return loading ? (
<>Loading...</>
) : error ? (
<>Failure: ${JSON.stringify(error)}</>
) : (
<>Success: ${JSON.stringify(data))</>
)
You should definitely check the documentation of useFetch<T> which you are using it should be written here.
Update for custom hook
In case of the hook from specified article, you should also consult the documentation for axios response. It clearly states that data are stored in so named attribute. It means that if you copied hook code as it is written article then it can't work. No part of the handler for Promise.then is using that value:
//checking for multiple responses for more flexibility
//with the url we send in.
res.data.content && setData(res.data.content);
res.content && setData(res.content);
It has to be fixed to access the response data correctly, for example:
// Give up the flexibility
setData(rest.?data)
// Keep some flexibility
// ... but you have to define data acquisition algorithm
// const data = res.content || rest.data
// setData(data)

ApolloGraphQL not triggering query when landing on page after a mutation that changes data

Intended result
I have two routes: /test/ and /test/:id.
On /test/ I have a list of events and it's only made of events that haven't been resolved
On /test/:id I have a mutation to mark an event as resolved, and, on success, I'm redirecting the user back to /test/.
This success means that the event should no longer appear on /test/ and I'm expecting a new request to get the list of events.
// my file with the mutation
const [eventResolveMutation] = useEventResolveMutation({
onCompleted: () => {
showSuccessToast(
`${t('form:threat-resolved')}! ${t('general:threat')} ${t(
'form:has-been-resolved'
)}.`
)
setTimeout(() => {
navigate('/threats/live')
}, 2000)
},
onError: (error: ApolloError) => {
showErrorToast(error.message)
},
})
const handleEventResolveClick = (id: string) => {
eventResolveMutation({ variables: { id: id, isResolved: true } })
}
return (
<button onClick={() => handleEventResolveClick(id)}>Press</button>
)
// my file with the `events` query
// the results are displayed in a table, which is way I have `currentPage` and `pageSize` in them
const [getEvents, { loading, data }] = useEventsLazyQuery()
useEffect(() => {
getEvents({
variables: {
page: currentPage,
pageSize: paginationSizeOptions[chosenDropdownIndex],
isThreat: true,
isResolved: false,
},
})
}, [chosenDropdownIndex, currentPage, getEvents])
Actual outcome:
Once I press the button that triggers the mutation and I'm redirected to the /tests, I can see that I'm landing inside the useEffect by logging something. What I don't see is a request made via getEvents, which is expected to happen since all the functionalities with the page work
Extra info:
// my graphqlclient.ts
import {
ApolloClient,
ApolloLink,
createHttpLink,
InMemoryCache,
} from '#apollo/client'
const serverUrl = () => {
switch (process.env.REACT_APP_ENVIRONMENT) {
case 'staging':
return 'env'
case 'production':
return 'env'
default:
return 'env'
}
}
const cleanTypeName = new ApolloLink((operation, forward) => {
if (operation.variables) {
const omitTypename = (key: string, value: any) =>
key === '__typename' ? undefined : value
operation.variables = JSON.parse(
JSON.stringify(operation.variables),
omitTypename
)
}
return forward(operation).map((data) => data)
})
const httpLink = createHttpLink({
uri: serverUrl(),
credentials: 'include',
})
const httpLinkWithTypenameHandling = ApolloLink.from([cleanTypeName, httpLink])
const client = new ApolloClient({
link: httpLinkWithTypenameHandling,
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
},
})
export default client
// my mutation
// this mutation will mark an `id` as `resolved` and that means that it should disappear from the list above
mutation EventResolve($id: ID!, $isResolved: Boolean!) {
eventResolve(id: $id, isResolved: $isResolved) {
id
sequence
}
}

Can't use new redux state right after fetching a response from Socket.IO

I have a function "sendMessage" in React class:
class MessageForm extends React.Component {
...
sendMessage = async () => {
const { message } = this.state;
if (message) {
this.setState({ loading: true });
if (this.props.isPrivateChannel === false) {
socket.emit("createMessage", this.createMessage(), (response) => {
this.setState({ loading: false, message: "", errors: [] });
});
} else {
if (this.state.channel && this.state.channel._id === undefined) {
socket.emit("createChannelPM", this.state.channel, async (response) => {
const chInfo = { ...response, name: this.props.currentChannel.name };
console.log("chInfo : ", chInfo);
await this.props.setCurrentChannel(chInfo).then((data) => {
if (data) {
console.log("data : ", data);
console.log("this.props.currentChannel : ", this.props.currentChannel);
}
});
});
}
...
function mapStateToProps(state) {
return {
isPrivateChannel: state.channel.isPrivateChannel,
currentChannel: state.channel.currentChannel,
};
}
const mapDispatchToProps = (dispatch) => {
return {
setCurrentChannel: async (channel) => await dispatch(setCurrentChannel(channel)),
}
};
Here, in sendMessage function, I retrieve "response" from socket.io, then put this data into variable "chInfo" and assign this to Redux state, then print it right after assinging it.
And Redux Action function, "setCurrentChannel" looks like:
export const setCurrentChannel = channel => {
return {
type: SET_CURRENT_CHANNEL,
payload: {
currentChannel: channel
}
};
};
Reducer "SET_CURRENT_CHANNEL" looks like:
export default function (state = initialState, action) {
switch (action.type) {
case SET_CURRENT_CHANNEL:
return {
...state,
currentChannel: action.payload.currentChannel
};
...
The backend Socket.io part look like (I use MongoDB):
socket.on('createChannelPM', async (data, callback) => {
const channel = await PrivateChannel.create({
...data
});
callback(channel)
});
The console.log says:
Problem : The last output, "this.props.currentChannel" should be same as the first output "chInfo", but it is different and only print out previous value.
However, in Redux chrome extension, "this.props.currentChannel" is exactly same as "chInfo":
How can I get and use newly changed Redux states immediately after assinging it to Redux State?
You won't get the updated values immediately in this.props.currentChannel. After the redux store is updated mapStateToProps of MessageForm component is called again. Here the state state.channel.currentChannel will be mapped to currentChannel. In this component you get the updated props which will be accessed as this.props.currentChannel.
I believe you want to render UI with the latest data which you which you can do.

React Redux doesn't return expected state

I have a like function on the backend (Node, MongoDB) that returns the given post with updated likes counter. This works, tested it with Postman. This is just an object with a bunch of properties like likes, _id., by, createdAt and so on...
let p = await Post.findById(req.params.id).populate("by");
return res.json(p);
Then I have a like action in React:
export const like = (id) => (dispatch) => {
const token = localStorage.getItem("token");
if (token) {
axios
.put(`http://localhost:5000/likePost/${id.id}`, id, {
headers: { "X-Auth-Token": token },
})
.then((res) => {
dispatch({
type: LIKE,
payload: res.data,
});
});
}
};
And I have a LIKE reducer:
case LIKE:
return {
...state,
posts: state.posts.map((p) => {
return { ...p };
}),
};
The LIKE reducer triggers when I click on the button and on the backend I can see the update but on the client side it doesn't update. I use redux-logger and the posts state is not updated.
What did I do wrong? I thought that spreading all the posts (...p) will update it, since it is updated on the backend.
This one works:
case LIKE:
return {
...state,
posts: state.posts.map((p) => {
if (p._id === action.payload._id) {
p.likes = action.payload.likes;
}
return p;
}),
};

async/await - dispatching redux actions: dispatching an action causes my app to freeze

I am creating an app using Expo SDK 26 - I am working on a piece where I need to upload a photo into firebase database. For android and ios - Expo provides Image Picker to allow native access to gallery/capturing an image.
When I receive an image - I am trying to capture the image uri, fetch the image and upload it to firebase storage with a reference to the images firebase storage url saved in my firebase database.
When I select the image from a device, my action creator is triggered, but when I dispatch an action - the app stops at the dispatch. Am I missing something obvious, or is the pattern not correct?
If so - what would be a good approach to this.
Select Image From Device Code:
async _pickImage(){
let result = await ImagePicker.launchImageLibraryAsync({
allowsEditing: true,
aspect: [4, 3],
});
if (!result.cancelled) {
this.props.uploadPhoto(result.uri)
}
};
Upload Image Code:
export const uploadPhoto = (uri) => {
return async (dispatch) => {
dispatch({
type: ActionTypes.UPLOAD_PROFILE_IMAGE_REQUEST
})
const response = await fetch(uri);
const blob = await response.blob();
const currentUser = firebase.auth().currentUser
const userId = currentUser.uid;
const ref = firebase.storage().ref().child(userId);
const databaseRef = firebase.database().ref().child("/users/" + userId + "/user-details")
await ref.put(blob)
.then((snapshot) => {
databaseRef.update({
"photoURL": snapshot.downloadURL
}).then(() => {
console.log(snapshot.downloadURL)
dispatch({
type: ActionTypes.UPLOAD_PROFILE_IMAGE_SUCCESS,
payload: snapshot.downloadURL
})
})
})
.catch((error) => {
dispatch({
type: ActionTypes.UPLOAD_PROFILE_IMAGE_FAILURE,
error: error
})
})
}
}
Reducer: UPLOAD_PROFILE_IMAGE_REQUEST
import * as ActionTypes from '../ActionTypes'
const INITIAL_STATE = {
userInfo: {},
error: "",
isLoading: false,
image: undefined
};
export default (state = INITIAL_STATE , action) => {
switch (action.type) {
case ActionTypes.GET_USER_DETAILS_REQUEST:
return {...state, isLoading: true }
case ActionTypes.GET_USER_DETAILS_SUCCESS:
return {...state, isLoading: false, userInfo: action.payload}
case ActionTypes.GET_USER_DETAILS_FAILURE:
return {...state, isLoading: false, error: action.payload }
case ActionTypes.UPLOAD_PROFILE_IMAGE_REQUEST:
return {...state, isLoading: true}
case ActionTypes.UPLOAD_PROFILE_IMAGE_SUCCESS:
return {...state, image: action.payload, isLoading: false}
case ActionTypes.UPLOAD_PROFILE_IMAGE_FAILURE:
return {...state, error: action.payload, isLoading: false}
case ActionTypes.CLEAR_USER_DETAILS:
return {INITIAL_STATE, isLoading: false}
default:
return state
}
}
I have tried console.log directly after the first dispatch but nothing prints in the log after the UPLOAD_PROFILE_IMAGE_REQUEST action dispatch.
any help is much appreciated.

Categories