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)
Related
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
}
}
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.
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;
}),
};
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]);
I have a function that has a bit of a promise chain going on, but that's besides the point.
When I run a certain mutation's refetch, it gives me Uncaught (in promise) TypeError: Cannot read property 'refetch' of undefined.
The strange part is that if I remove a mutation before it, it will work. So here's the code:
Promise.all(this.props.questionnaireData.map(({ kind, id }): Promise<any> => {
const responses = this.props.formData[kind];
return this.props.updateQuestionnaire(id, responses);
})).then(() => {
this.props.finishAssessment(this.props.assessmentId)
.then(() => {
track('Assessment -- Finished', {
'Assessment Kind' : this.props.assessmentKind,
'Assessment Id' : this.props.assessmentId,
});
if (this.props.assessmentKind === 'INITIAL_ASSESSMENT') {
this.props.getCompletedInitialAssessment.refetch().then(() => {
Router.replace(routes.LoadingAssessmentResults.to, routes.LoadingAssessmentResults.as);
});
this.submitEmailNotifications();
} else if(this.props.assessmentKind === 'GOAL_CHECK_IN') {
Router.replace(routes.MemberProgressDashboard.to, routes.MemberProgressDashboard.as);
} else {
Router.replace(routes.MemberDashboard.to, routes.MemberDashboard.as);
}
});
});
The error happens at this.props.getCompletedInitialAssessment.refetch(), to which I don't know why. However, when I remove this.props.finishAssessment(this.props.assessmentId), only then the refetch will work.
Basically:
Promise.all(this.props.questionnaireData.map(({ kind, id }): Promise<any> => {
const responses = this.props.formData[kind];
return this.props.updateQuestionnaire(id, responses);
})).then(() => {
track('Assessment -- Finished', {
'Assessment Kind' : this.props.assessmentKind,
'Assessment Id' : this.props.assessmentId,
});
if (this.props.assessmentKind === 'INITIAL_ASSESSMENT') {
this.props.getCompletedInitialAssessment.refetch().then(() => {
Router.replace(routes.LoadingAssessmentResults.to, routes.LoadingAssessmentResults.as);
});
this.submitEmailNotifications();
} else if(this.props.assessmentKind === 'GOAL_CHECK_IN') {
Router.replace(routes.MemberProgressDashboard.to, routes.MemberProgressDashboard.as);
} else {
Router.replace(routes.MemberDashboard.to, routes.MemberDashboard.as);
}
});
will make refetch work. Otherwise it complains that it doesn't know what refetch is.
For Apollo, I'm using the graphql HOC, and it looks like this:
graphql(getCompletedInitialAssessment, {
name : 'getCompletedInitialAssessment',
options : { variables: { status: ['Finished'], limit: 1 } },
}),
graphql(updateQuestionnaire, {
props: ({ mutate }) => ({
updateQuestionnaire: (id, responses) => {
let normalized = {};
for (let res in responses) {
let num = +responses[res];
// If the value is a stringified numuber, turn it into a num
// otherwise, keep it a string.
normalized[res] = Number.isNaN(num) ? responses[res] : num;
}
const input = {
id,
patch: { responses: JSON.stringify(normalized) },
};
return mutate({
variables: { input },
});
},
}),
}),
graphql(finishAssessment, {
props: ({ mutate }) => ({
finishAssessment: (id) => {
const input = { id };
return mutate({
variables : { input },
refetchQueries : ['getMemberInfo'],
});
},
}),
}),
What I've tried is even rewriting this to use async/await, but the problem still happens:
try {
await Promise.all(this.props.questionnaireData.map(({ kind, id }): Promise<any> => {
const responses = this.props.formData[kind];
return this.props.updateQuestionnaire(id, responses);
}));
const finishAssessmentRes = await this.props.finishAssessment(this.props.assessmentId);
console.log(finishAssessmentRes)
if (this.props.assessmentKind === 'INITIAL_ASSESSMENT') {
const res = await this.props.getCompletedInitialAssessment.refetch();
console.log(res);
this.submitEmailNotifications();
Router.replace(routes.LoadingAssessmentResults.to, routes.LoadingAssessmentResults.as);
} else if(this.props.assessmentKind === 'GOAL_CHECK_IN') {
Router.replace(routes.MemberProgressDashboard.to, routes.MemberProgressDashboard.as);
} else {
Router.replace(routes.MemberDashboard.to, routes.MemberDashboard.as);
}
} catch (error) {
console.error(error);
}
I honestly don't know what's happening or why refetch wouldn't work. Would refactoring into hooks help? Does anyone have any idea?
From the docs
The config.props property allows you to define a map function that takes the props... added by the graphql() function (props.data for queries and props.mutate for mutations) and allows you to compute a new props... object that will be provided to the component that graphql() is wrapping.
To access props that are not added by the graphql() function, use the ownProps keyword.
By using the props function, you're telling the HOC which props to pass down either to the next HOC or to the component itself. If you don't include the props that were already passed down to it in what you return inside props, it won't be passed to the component. You need do something like this for every props function:
props: ({ mutate, ownProps }) => ({
finishAssessment: (id) => {
//
},
...ownProps,
}),
Composing HOCs is a pain and the graphql HOC is being deprecated anyway in favor of hooks. I would strongly advise migrating to the hooks API.