-
Notifications
You must be signed in to change notification settings - Fork 53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(auto-save): Auto-save on custom / page type & slice builder #1258
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
9e71dfa
to
4c92702
Compare
4c92702
to
b412472
Compare
b412472
to
9a85c8a
Compare
9a85c8a
to
8aa625c
Compare
8aa625c
to
2f17bed
Compare
2f17bed
to
eafae8c
Compare
eafae8c
to
8b7d010
Compare
8b7d010
to
621123a
Compare
621123a
to
bc680c1
Compare
bc680c1
to
c130f36
Compare
c130f36
to
cc3118f
Compare
cc3118f
to
6449ea8
Compare
6449ea8
to
6beab0a
Compare
8606db8
to
b9a8a9b
Compare
b9a8a9b
to
1bc7048
Compare
1bc7048
to
5ff0f6e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice useAutoSave
hook 😊 Just have some questions and comments as I don't understand all the intricacies of it.
nextSave: (() => Promise<unknown>) | null; | ||
}; | ||
|
||
export type AutoSaveStatusType = "saving" | "saved" | "error"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type
to a type
.
💡 Maybe failed
would be better than error
? That way, you'd be using past tense like saved
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did that because I have a visual component named AutoSaveStatus
that also refer to the type. So I wanted to be clear about the difference. WDYT?
Good idea for failed
, I will rename it 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, what do you think about naming the component AutoSaveStatusIndicator
then? 😊
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good idea, yes!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 I wonder if a new core/autoSave
folder would be a better fit. Indeed, I feel like we're putting too many things into features
. For me, features
should ideally be "optional" parts/pages/modules and core
should contain the shared domain bits that allow the features
to work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see. I really like it, we kind of discussed a bit that at the beginning I remember. But maybe it will also create a problem with too many first level folder in src with a dev never knowing where to put stuff?
For me, I "feel" it's ok, but I wanted to warn about that.
If we create a core
folder, we could put:
- autoSave
- documentation
- environments
- inAppGuide
- legacyState
- slicesTemplates
And so in features we leave:
- changes
- cusotmTypes
- labs
- slices
WDYT?
Btw I guess since it's already a +3000/-3000 PR I can do that right after my PRs are merged and only do the autoSave now. So I ease the review for Hugo. Ok for you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay for me, but I'm not sure about the partitioning you propose. We'll discuss it again ^^
Also, what would you think of moving all the "old" code into a separate top-level old
directory? 😜
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What old code you refer to? I'm not a fan of this, then you start to have again more confusion and folders. The old folders and old code. I feel It's just more pain to maintain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following our discussion, will remove 2 folders after my PRs
}; | ||
|
||
type AutoSaveState = { | ||
pendingSave: (() => Promise<unknown>) | null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
undefined
over null
for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep my bad, I prefer also to use undefined, was too fast here. Good catch, thanks 🙂
I will also add it to the best practices notion page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
errorMessage = "An error happened while saving", | ||
retryDelay = 2000, | ||
retryMessage = "Retry", | ||
} = args; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 A line break here would improve readability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
||
export type AutoSaveStatusType = "saving" | "saved" | "error"; | ||
|
||
export const useAutoSave = (args: UseAutoSaveArgs = {}) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep good catch, I will do it, and also it's already in the best practices 🎉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
await autoSaveState.pendingSave(); | ||
|
||
setAutoSaveStatusInternal("saved"); | ||
setAutoSaveState((prevState) => ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ Is this necessary as it's already done in the useEffect
below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hum good thinking. I will try to see if it's possible and test it. It may simplify a bit the useEffect bellow even maybe.
But I'm not sure. I will try and come back to you 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes!
So I fixed the bug of the retry, and I really took time to check that one.
When a save fail, we want to trigger a retry with the latest data we have (saw with Benjamin), meaning we don't want to just reply the pending. We may want to retry with the next, but if there is no next, meaning the user did not do any action in the meantime, we will retry with the pending.
Because of this it's important in case of a success to remove the pending, if not the useEffect will detect that there is a pending operation and will try to do it again, creating an endless loop.
I added comments for the tricky parts, btw.
toast.dismiss(toastId); | ||
setAutoSaveStatusInternal("saving"); | ||
setTimeout(() => { | ||
void executeSave(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ I'm not sure I understand all the execution branches that could happen. For instance, what happens if:
- There's an error.
- The user doesn't click on the retry button, but performs another action in the UI.
- The user clicks on the retry button.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed, I don't trigger the next save if they is an error. The retry will do it only. But any changes done after a fail will still but stacked.
autoSaveStatusInternal !== "saving" && | ||
(autoSaveState.pendingSave || autoSaveState.nextSave) | ||
) { | ||
setAutoSaveState((prevState) => ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ I'm not sure I understand well this side effect. It seems it will run if autoSaveStatusInternal
is error
and the pendingSave
corresponding to that error would be replaced by nextSave
. Is it intended? Should the toast be dismissed as executeSave
will automatically be called?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, this one is the one that actually trigger every next save.
Meaning, when we want to perform a next save we check, is there any pendingSave or nextSave WHILE the status is not "saving" and so doing something. If yes, let's run the nextSave or just remove the pending one.
About the error, yes I think I need to dismiss somewhere to prevent the error above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useEffect updated with the fix on the error and it should be clearer now. With a comment explaining the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, excellent work Xavier 😊
|
||
type CustomTypeContext = { | ||
customType: CustomType; | ||
autoSaveStatus: AutoSaveStatusType; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ It means for now we have one autosave state for custom types. Is it going to be a different state for slices? How are we going to combine both autosave states to display the error toasts and status messages consistently/once?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For me, I was planning to have 2 providers. As the provider, don't have the save logic since it's the hook and only contain things purely related to custom types or slices.
There are some duplications, but I would say healthy one that help us maintain the project without risking crashing everything.
You can see what I'm planning to do here: https://github.com/prismicio/slice-machine/blob/xru/auto-save-slice-builder/packages/slice-machine/src/features/slices/sliceBuilder/SliceBuilderProvider.tsx
And you can see that there are difference related to slices.
So for me 2 providers that reuse the hook yes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I just wonder how the 2 autoSaveStatus
will be combined to provide a clear visual indicator to the users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hum, they will never be combined, since it's display from within the builder you never have both in the same view.
|
||
type CustomTypeProviderProps = { | ||
children: ReactNode | ((value: CustomTypeContext) => ReactNode); | ||
defaultCustomType: CustomType; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 I think initialCustomType
would be more in line with the parameter name of the useState
hook.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes good point! I think naming is my nemesis! 😈
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
packages/slice-machine/src/features/customTypes/customTypesBuilder/CustomTypeProvider.tsx
Show resolved
Hide resolved
|
||
const CustomTypeContextValue = createContext<CustomTypeContext>({ | ||
autoSaveStatus: "saved", | ||
customType: {} as CustomType, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CustomTypeProvider
, he'll get a customType
that doesn't conform to the CustomType
interface. I think it's both safer and clearer to initialize the context with undefined
and throw if the provider cannot be found.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was never a fan of how it's done in React. I would just love to have a provider where I initialize with undefined but every time I'm getting the context typescript knows that it's full. And React handle the error if I didn't setup the Prodiver.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do it 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
}); | ||
|
||
export function CustomTypeProvider(props: CustomTypeProviderProps) { | ||
const { children, defaultCustomType } = props; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Adding a new line after the props spreading would make the code clearer in my opinion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will add it, maybe it's something I will look for now. I tend to group it like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
||
export function CustomTypeProvider(props: CustomTypeProviderProps) { | ||
const { children, defaultCustomType } = props; | ||
const isMounted = useRef(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Maybe you could use the useIsFirstRender hook from @prismicio/editor-support
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
100%, I didn't even know about it. It's something we (expect you haha) are not doing. More looking for simple stuff in editor packages. I will use it!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
isMounted.current = true; | ||
} | ||
}, | ||
// Prevent saveCustomTypeSuccess to trigger infinite loop |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Maybe "from triggering an infinite loop" would be better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
console.error("Error in save effect:", error), | ||
); | ||
} | ||
}, [autoSaveStatusInternal, autoSaveState, executeSave]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe every property of autoSaveState
should be listed in here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, it's not necessary here since it's immutable, we are talking about a state so each time the values changed the state itself 100% change. You never modify a state sub value directly.
The linter is pretty good on that and tell us here that it's good.
5ff0f6e
to
9da363e
Compare
9da363e
to
23667df
Compare
916f31f
to
9d769bd
Compare
9d769bd
to
6694bd6
Compare
6694bd6
to
00a2b21
Compare
export function CustomTypeProvider(props: CustomTypeProviderProps) { | ||
const { children, initialCustomType } = props; | ||
|
||
const isFirstRender = useIsFirstRender(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
}, | ||
// Prevent saveCustomTypeSuccess from triggering an infinite loop | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
[customType, setNextSave], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it better to remove it from the deps or the wrap it with a useCallback?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could try to use useStableCallback
from the editor support, good idea!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
51a9163
to
02dc287
Compare
Context
The Solution
Auto-save for page / custom type:
selectedCustomTypeReducer
reducerwatchSelectedCustomTypeSagas
sagauseAutoSave
to ensure we have the proper way to handle multiple changesTO DO
Impact / Dependencies
Checklist before requesting a review
Preview
Screen.Recording.2023-12-29.at.18.53.35.mov