I would love a short code review of "my" implementation of optimistic ui patterns. I'm using SWR, immer and a custom fetch hook to do most of the heavy lifting. However, I'm not really sure if this is indeed the way to do it. Especially when it comes to asigning a temporary id to the optimistically generated item. Shouldn't I clear it somewhere? May it cause issues?
const spawn = async flavour => {
const payload = {
planId: flavour.id,
name: 'test',
description: '',
}
mutate(
'/account/instances',
produce(draft => {
draft.push({
...payload,
id: uuid(),
plan: {name: flavour.name},
state: {status: 'PENDING'},
image: {name: flavour.image.name},
})
}),
false
)
mutate(
'/account/instances',
await doFetch('/account/instances', 'post', payload)
)
}
Thanks!
Your code seems correct to me. It will update optimistically the UI, fetch data from the backend, and update again the UI with that data if it differs.
Regarding the generated id, it really depends on what you do with it. If you do nothing important it will probably be ok as that id will be overwritten with the real one when the backend replies. But it may cause problems if you display it to the user (the user will see it being updated), or even worse if you provide an action based on it.
I would also like to draw your attention on the fact that the reply may fail for network reasons, or reasons related to a problem on backend side. In that case, if you receive just an error or never receive any reply, your UI will remain in an incorrect state until the next successful fetch made by swr.
To avoir this kind of problem, you may be interested in use-mutation. It's a small lib designed to be used with swr to rollback the optimistically updated data in case of error.
Related
I use RTK Query for data fetching and I am having a small issue in one of the use cases.
I use a mutation to verify the email address of the app user, as follows:
// ...imports
const ConfirmEmail = () => {
const [params] = useSearchParams();
const [confirmEmail, { isLoading, isSuccess, isUninitialized, error }] = useConfirmEmailMutation();
useEffect(() => {
if (isUninitialized) {
confirmEmail({
email: params.get('email'), // from the verification link
code: params.get('code'), // from the verification link
})
.unwrap()
.then(() => {
// handling success...
})
.catch((e) => {
// handling error...
});
}
});
// ...other component code
}
The problem is that with StrictMode the mutation is running twice in development and it is causing a concurrency error at the API side. I see two network requests in the dev tools one is successful and the other is not and depending on which one runs first the component is showing inconsistent result.
I am aware that this will only happen during development and I tried to follow the instructions in the official react documentation and I tried to use fixedCacheKey as described here but I wasn't able to make this work with RTK Query so that I get a consistent result during development.
Is there a way, or am I missing something?
That's pretty much the point of this strictness check though: to show you that you are automatically doing a problematic api call.
In the future, React can choose to execute a useEffect with an empty dependency array on more occasions than just on first mount - for example in combination with the offscreen features they are working on.
This just tells you in advance that you are doing this in probably a dangerous place.
Can you maybe incorporate this api call in some kind of actual user interaction like a button click? That would be a lot safer going forward.
I'm interested in opinions on how to manage global client state (Zustand) invalidation as a result of a socket update which modifies the network state cache (Apollo).
Let's say we have the following state:
Apollo cache (requested from server):
{ posts: [
{ id: 1, text: 'Hello World!' },
{ id: 2, text: 'I has a bucket!' },
{ id: 3, text: 'Thanks StackOverflow!' }
] }
Zustand store (client state):
{
selectedPost: 2,
editMode: true
}
In this state, we show a UI which allows editing the contents of post 2. We run into an edge case however, if another user deletes post 2. A socket update will remove that post from our network cache, while our client state still believes it is selected and being edited.
This could result in a strange user experience where the post has disappeared, but they still see the edit UI, and any attempt to save will fail, since the resource no longer exists to be updated on the backend.
The expectation is to set the following client state if the selected feedback is removed:
{
selectedPost: null,
editMode: false,
}
The full situation is more complex, and I'd want to show a message to the user, but I feel those details are unimportant to this discussion, so I've minimized the state as much as possible.
I've considered a few options for solving this problem. Each with some pros and cons. Any one of these can work. What I'm interested in is advice from someone familiar with this type of problem, on which solution is most viable long-term.
useEffect
Write a useEffect which listens for posts not including a post with post.id === selectedPost, and updates client state to { selectedPost: null, editMode: false }.
This has the disadvantage that there is an in-between render where the post does not exist, but it is still selected and we still show the edit UI. Meaning we have to be sure our components can handle that invalid state. It could also result in a visual flicker where the user sees this invalid UI briefly before the client state is adjusted.
Side-effect of Apollo cache change
I'm not sure if this is something that can be done.
Write a callback which runs when the Apollo cache changes, but before the resulting render occurs. This callback checks if selectedPost exists in the new cache. If not, it updates the client state. This would not cause the in-between invalid render.
I haven't seen anything on the Apollo docs about this. So I'm not sure if it's possible.
Update client state in the socket handler, at the same time Apollo cache is updated
We have functions that handle each socket event, which make the required modifications to the Apollo cache. We could add the client state checks and updates here. This would avoid the invalid render problem.
The downside is we would have to manually handle every socket event that could result in removing a post. Which would result in duplicated code and room for error if new socket events were introduced.
Does anyone have experience with updating client state as a side-effect of network state like this? Any suggestions on how to handle it elegantly without the invalid in-between render?
What I am trying to do:
useMutation to edit the email value and re render the page. The data changes on refresh but the cache needs to change as well and I am having trouble modifying the cache.
Legwork I have read/looked over before posting here:
Documentation on Mutations
Documentation on Updating cache
Obtaining an Object's Custom ID
What I think the problem is:
the GraphQL database I am using does not implement a proper ID and I can not change that. I will have to modify the ROOT_QUERY cache and update it the hard way. As my code is right now It's reading the ID I want to use (email) as undefined. The error message simply reads as "Unhandled Rejection (Error): Cannot read property 'email' of undefined" pointing to functions in the node modules and nowhere near my function.
My component's code can be found in full here, but I will point to where I think the smoking gun ultimately is.
I have tried checking as much as I can but have no way of telling if i am reaching things correctly. I have looked at other stack posts, youtube, and tried reaching out to a few discord channels only to hit a wall. Any help and explanation would be great.
Figured out the problem was cache.identify was not needed at all! I just had to fix the InMemoryCahce at the index.tsx
const client = new ApolloClient({
uri: "http://localhost:8080/graphql",
cache: new InMemoryCache({
typePolicies: {
People: {
keyFields: ["email"],
},
},
}),
});
This made things easier on getting the right index.
I have read multiple articles on the need to use Redux and have built two fully-functioning React+Redux applications. I have even posted the question on Quora I still cannot have a final answer to my question:
Do I have to save every component state property to the Redux store?
The first project, I have built by following a tutorial where he basically saves everything to the store.
Here's the Github link.
Since I was learning React and Redux, I did not question this approach and went on with it. But, it does seem somewhat unnecessary to save everything to the store
For example, there's an action that saves the comment data to the store:
postActions.js
// Add Comment
export const addComment = (postId, commentData) => dispatch => {
dispatch(clearErrors());
axios
.post(`/api/posts/comment/${postId}`, commentData)
.then(res =>
dispatch({
type: GET_POST,
payload: res.data
})
)
.catch(err =>
dispatch({
type: GET_ERRORS,
payload: err.response.data
})
);
};
And it is called like this:
CommentForm.js
onSubmit(e) {
e.preventDefault();
const { user } = this.props.auth;
const { postId } = this.props;
const newComment = {
text: this.state.text,
name: user.name,
avatar: user.avatar
};
this.props.addComment(postId, newComment);
this.setState({ text: '' });
}
If I were working on my own project, I would've kept the message data stored locally at the component level:
The second project was a personal project, where the only data I saved in the store, is the user account information because I would need it in different components throughout the app to send it in some backend API requests.
All the other components are basically independent or the flow between them does not go beyond two or three components. So I really could not see why I would make myself code all the actions, reducers...etc for all of the components. So I simply pass the props and functions between components in the plain old react way of doing things.
Most of the answers that I found do not go into this specific detail mentioned in my question. All of them talk from a high-level perspective.
Before going ahead and working on other projects, I would like to :
A clear answer to my question
Whether the approach I used for my personal project is okay. In other words, can I use Redux simply for the user account information and for the rest of the components not use it?
I just want to clear this confusion so that when I am using Redux, I am 100% sure, I am using it because I actually need it.
Do I have to save every component state property to the Redux store?
Short answer: No you don't.
Longer answer: To quote Dan Abramov on a similar question:
Use React for ephemeral state that doesn't matter to the app globally and doesn't mutate in complex ways. For example, a toggle in some UI element, a form input state.
Use Redux for state that matters globally or is mutated in complex ways. For example, cached users, or a post draft.
There is nothing wrong with the approach taken in your personal project. Redux is great for storing/sharing global application state, such as the user info you describe.
Before putting state into Redux I'd ask:
Will this state be consumed by other components independent to this one?
If the answer to #1 is yes: then ask how often?
If the answer to #2 is frequently: then ask is a single source of truth (the Redux store) the best way to share this particular piece of state? Would
other techniques (hooks / render props / higher order components) be more appropriate?
Another quote from Dan in the same linked thread is:
If it gets tedious and frustrating don’t be afraid to put state into the components. My point is that use single state tree unless it is awkward, and only do this when it simplifies things for you rather than complicates them. That’s the only guideline.
The mantra Yagni (You Aren't Gonna Need It) springs to mind.
If you're unsure whether state should be abstracted from a component into Redux, then the chances are it's too early todo so. This helps avoid making design decisions too early, whilst keeping your Redux state lean and intentional (i.e: not convoluted with unnecessary single use concerns).
Ultimately the cost of putting state into Redux needs to pay off.
So I'm using Apollo for my app's state, and am so far a little taken aback there's no equivalent to mapStateToProps or something.
As far as I understand, to have any data globally accessible in my store, once I get the data, I need a query to write the data to the store, then another query in my other component to go and get it.
By this point, the other component has very much mounted and rendered, so content just sort of flickers in and out.
In Redux, I can just add new data to the store in my reducers, then anything that's connected with mapStateToProps has access to it.
Is there an equivalent? Or does everything need to go through asynchronous queries? Does anyone else kind of find this an enormous pain?
For example, in one component I'm getting some invitation data:
this.props.client.query({
query: REQUEST_ACTION_DATA,
variables: {
id: actionData.id,
type: actionData.type
}
}).then(data => {
this.props.client.writeQuery({query: GET_ACTION_DATA, data: {
action: {
type: data.data.actionData.type,
object: data.data.actionData.invitation,
__typename: 'ActionDataPayload'
}
}})
this.props.history.push('/auth/register')
})
... then in my other component I have this:
componentWillMount() {
const authToken = localStorage.getItem(AUTH_TOKEN);
if (authToken) {
this.props.history.push('/')
}else{
this.props.client.query({
query: GET_ACTION_DATA
}).then(data => {
if(data.data && data.data.action && data.data.action.type == 'invite'){
this.setState({
invitation: data.data.action.object
})
}
console.log(data)
})
}
}
ignoring the fact that it's hugely unwieldy to write all this for something so simple, is there just a way to access store data without having to wait around?
The graphql higher order component from react-apollo is a supercharged connect from redux. It will do the following:
When the component mounts it will try and execute the query on the Apollo cache. You can think of the query as a DSL for selecting data not only from the server but also from the store! If this works the component is pretty much instantly filled with the data from the store and no remote requests are executed. If the data is not available in the store it triggers a remote request to receive the data. This will then set the loading property to true.
The Apollo cache is a normalised store very similar to your redux store. But thanks to the well thought through limitations of GraphQL the normalisation can happen automatically and you don't have to bother with the structure of the store. This allows the programmer to forget about stores, selectors and requests altogether (unless you are heavily performance optimising your code). Welcome to the declarative beauty of GraphQL frontend frameworks! You declare the data you want and how it gets there is fully transparent.
export default graphql(gql`{ helloWorld }`)(function HelloWorld({ data }) {
if (data.loading) {
return <div>Loading the simple query, please stand by</div>;
}
return <div>Result is: {data.helloWorld}</div>;
});
No selector, no denormalisation happening. This is done by Apollo Client! You will get updated data when the cache updates automatically similar to Redux.
No componentWillMount checks for data is in store already and triggering request actions. Apollo will make the checks and trigger queries.
No normalisation of response data (also done by Apollo Client for you)
If you do any of the above you are probably using Apollo Client wrongly. When you want to optimise your queries start here.