09 Ιουλ, 2022

Υλοποιηση "useUndoRedo" hook

React undo redo

Είμαι αρκετά σίγουρος ότι έχετε κλικάρει αρκετές φορές στη ζωή σας το "Undo-Redo". Εγώ ξέρω ότι το έχω κάνει. Και οι δυο αυτές λειτουργίες είναι αρκετές σημαντικές σε ένα workflow καθώς μας επιτρέπουν να "ταξιδέυουμε" μπρος-πίσω στο χρόνο με σκοπό να διορθώνουμε πιθανά λάθη.

Αναρωτηθήκατε ποτέ πώς θα φαινόταν μια υλοποίηση undo-redo σε ένα πρόγραμμα υπολογιστή; Είμαι σίγουρος ότι υπάρχουν πληθώρα υλοποίησεων εκεί έξω αλλά τα θεμέλια όλων αυτών είναι πάνω κάτω τα ίδια:

  1. Ένας μηχανισμός για να αποθηκεύει την παρούσα τιμή καθώς και τις παρελθοντικές και μελλοντικές τιμές.
  2. Ένας αλγόριθμος που θα αλλάζει το 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 παρελθοντικές και μελλοντικές τιμές.

React undo redo demo

Ο κώδικας του 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

Ελπίζω να βρήκατε το άρθρο χρήσιμο.

Τα λέμε αργότερα! 🙂

Loading Comments...