09 Ιουλ, 2022
Υλοποιηση "useUndoRedo" hook
Είμαι αρκετά σίγουρος ότι έχετε κλικάρει αρκετές φορές στη ζωή σας το "Undo-Redo". Εγώ ξέρω ότι το έχω κάνει. Και οι δυο αυτές λειτουργίες είναι αρκετές σημαντικές σε ένα workflow καθώς μας επιτρέπουν να "ταξιδέυουμε" μπρος-πίσω στο χρόνο με σκοπό να διορθώνουμε πιθανά λάθη.
Αναρωτηθήκατε ποτέ πώς θα φαινόταν μια υλοποίηση undo-redo σε ένα πρόγραμμα υπολογιστή; Είμαι σίγουρος ότι υπάρχουν πληθώρα υλοποίησεων εκεί έξω αλλά τα θεμέλια όλων αυτών είναι πάνω κάτω τα ίδια:
- Ένας μηχανισμός για να αποθηκεύει την παρούσα τιμή καθώς και τις παρελθοντικές και μελλοντικές τιμές.
- Ένας αλγόριθμος που θα αλλάζει το state κατάλληλα ανάλογα με το undo, redo και set action.
Βάσει των παραπάνω, πιστεύω πώς ένας Reducer ταιριάζει τέλεια στην περίπτωση. Είναι μια συνάρτηση η οποία παίρνει δυο ορίσματα - το τρέχον state και ένα action - και επιστρέφει βάσει αυτών των ορισμάτων ένα καινούργιο state.
Ας δοκιμάσουμε να σκεφτούμε πώς μπορούμε να αξιοποιήσουμε ένα reducer για να πετύχουμε ένα undo/redo μηχανισμό.
Βήμα 1: Χρειάζεται να αποθηκεύουμε τις παρελθοντικές και μελλοντικές τιμές μαζί με την παρούσα τιμή.
Ένας τρόπος που μπορούμε να το πετύχουμε αυτό είναι με το να αποθηκεύουμε παρελθοντικές, τρέχουσες και μελλοντικές τιμές μέσα σε ένα object. Παρατηρήστε ότι οι παρελθοντικές και μελλοντικές τιμές πρέπει να είναι ένας πίνακας καθώς πρέπει να μπορούμε να "ταξιδεύουμε" όσο θέλουμε πίσω ή μπροστά στο χρόνο.
const state = {
past: [...] // past values
present: ... // present value
future: [...] // future values
}
Βήμα 2: Ένας αλγόριθμος για undo, redo και set action.
Για να εξηγήσουμε καλύτερα τον αλγόριθμο ας υποθέσουμε ότι το state του counter είναι κάπως έτσι κάποια στιγμή στην εφαρμογή μας:
const state = {
past: [0, 1, 2, 3]
present: 4
future: [5, 6]
}
Undo Action
Σε ένα undo action, πρέπει να κοιτάξουμε στο παρελθόν χωρίς όμως να χάσουμε το παρόν. Οπότε:
- Past: Η τελευταία τιμή του past αφαιρείται από τον πίνακα.
- Present: Παίρνει την τελευταία τιμή του past (η οποία αφαιρείται όπως αναφέρθηκε παραπάνω).
- Future: Κάνουμε append την τιμή του present στην αρχή του πίνακα future.
Άρα μετά το undo το state μας είναι:
const state = {
past: [0, 1, 2]
present: 3
future: [4, 5, 6]
}
Redo Action
Σε ένα redo, πρέπει να κοιτάξουμε στο μέλλον. Συνεπώς:
- Past: Κάνουμε push την τιμή present στο τέλος του πίνακα past.
- Present: Η πρώτη τιμή του πίνακα future γίνεται τώρα η τιμή present.
- Future: Αφαιρούμε το πρώτο στοιχείο του πίνακα future (το οποίο τώρα πια αντιπροσωπεύει το present).
const state = {
past: [0, 1, 2, 3]
present: 4
future: [5, 6]
}
Set Action
Πώς θα διαμορφωνόταν το state όταν λάβει χώρα ένα set action; Π.χ όταν ο μετρητής αυξηθεί η μειωθεί;
- Past: Το "παρόν" τώρα πια τώρα πια ανήκει στο παρελθόν. Οπότε κάνουμε push το present value στον πίνακα past.
- Present: Το σετάρουμε με την νέα τιμή.
- Future: Το μέλλον γίνεται clear.
const state = {
past: [0, 1, 2, 3, 4]
present: 5
future: []
}
useUndoRedo: Υλοποίηση με React hook
Ρίξτε μια ματιά σε αυτό το CodeSandbox: https://codesandbox.io/s/react-undo-redo-c3x8pw
Αξιοποιώντας το useReducer
hook υλοποιήσαμε ένα custom hook εν ονόματει useUndoRedo
το οποίο μοιάζει πολύ με το setState αλλά παρέχει undo-redo μηχανισμό καθώς και τις actual παρελθοντικές και μελλοντικές τιμές.
Ο κώδικας του useUndoRedo
hook φαίνεται παρακάτω.
// use-undo-redo.js
import { useReducer } from "react";
const SET_STATE = "SET_STATE";
const UNDO = "UNDO";
const REDO = "REDO";
const reducerWithUndoRedo = (state, action) => {
const { past, present, future } = state;
switch (action.type) {
case SET_STATE:
return {
past: [...past, present],
present: action.data,
future: []
};
case UNDO:
return {
past: past.slice(0, past.length - 1),
present: past[past.length - 1],
future: [present, ...future]
};
case REDO:
return {
past: [...past, present],
present: future[0],
future: future.slice(1)
};
default:
throw new Error();
}
};
const useUndoRedo = (initialState = {}) => {
const [state, dispatch] = useReducer(reducerWithUndoRedo, {
past: [],
present: initialState,
future: []
});
const { past, present, future } = state;
const setState = (newState) => dispatch({ type: SET_STATE, data: newState });
const undo = () => dispatch({ type: UNDO });
const redo = () => dispatch({ type: REDO });
const isUndoPossible = past && past.length > 0;
const isRedoPossible = future && future.length > 0;
return {
state: present,
setState,
undo,
redo,
pastStates: past,
futureStates: future,
isUndoPossible,
isRedoPossible
};
};
export default useUndoRedo;
The End
Ελπίζω να βρήκατε το άρθρο χρήσιμο.
Τα λέμε αργότερα! 🙂
Εγγραφή
Εγγραφειτε στην λιστα
Εγγραφείτε με το e-mail σας για να σας στέλνω το υλικό μου. Δεν θα είναι spam, σας το υπόσχομαι! Μπορείτε να καταργήσετε την εγγραφή σας όποτε θέλετε.