JavaScript réduit

The JavaScript ecosystem is thriving and moving quickly, but there’s finally an end at the light of the tunnel. Best practices are no longer changing constantly, and it is becoming increasingly clear which tools are worth learning.

The most important thing to remember is to keep it simple and only use what you need.

State of the Art JavaScript in 2016 (cache)

Il y a une certaine ironie à arriver à cette conclusion après avoir cité des lignes et des lignes d’outils :-). Pour faire suite à ma recherche de minimalisme et d’esthétique, j’ai commencé un projet un peu plus conséquent pour voir jusqu’où je pouvais aller. J’ai rapidement eu besoin de gérer des données au niveau du frontend de manière un peu plus sérieuse et je me suis penché sur le pattern remis au goût du jour par Flux. Plus particulièrement les principes qui sont utilisés dans Redux avec un état global mais en lecture seule qui n’est modifiable que par des reducers. Pour bien comprendre les concepts sous-jacents, je ne voulais pas utiliser Redux mais le découvrir en l’implémentant et réduire mes dépendances. Je suis parti d’un gist pour avoir la notion de Store :

const createStore = (reducer, state = reducer(undefined, {})) => {
  const subscribers = new Set()
  return {
    dispatch: (action) => {
      state = reducer(state, action)
      subscribers.forEach(func => func(action))
    },
    subscribe: (func) => {
      subscribers.add(func)
      return () => subscribers.delete(func)
    },
    getState: () => state
  }
}

On a ici trois méthodes qui permettent d’expédier des actions, d’abonner des fonctions et de récupérer l’état en cours (à partir de maintenant je passe aux termes techniques en anglais car ça devient n’importe quoi). Lorsqu’on récupère l’état de notre Store, on a toutes les données d’un coup que l’on peut explorer, j’avais déjà parlé de l’importance de pouvoir avoir cette vision avec TinyDB côté serveur pour développer efficacement. En tant que développeurs web, notre métier est de savoir quelles données correspondent à quelles URLs. Il y a bien sûr leur représentation finale et la suite de sérialisations et déserialisations qu’elles subissent mais pouvoir être sûr des données et des URLs c’est déjà un pré-requis rarement atteint. On va maintenant passer aux Reducers qui permettent de transformer l’état global avec des fonctions pures (= qui retournent de nouveaux objets). Ici la syntaxe est généralement de gérer les nombreux cas avec un switch assez laid, un petit constructeur va nous aider à rendre ça plus esthétique :

const createReducer = (initialState, handlers) => {
  return (state = initialState, action) => {
    if (handlers.hasOwnProperty(action.type)) {
      return handlers[action.type](state, action)
    } else {
      return state
    }
  }
}

Par défaut c’est l’état en cours qui est retourné si l’action n’est pas trouvée. Il nous reste à avoir un créateur d’actions pour ne pas avoir à taper à la main la logique standardisée pour chacune d’entre elles :

const createAction = (type, ...argNames) => {
  return (...args) => {
    let action = { type, payload: {} }
    argNames.forEach((arg, index) => {
      action.payload[argNames[index]] = args[index]
    })
    return action
  }
}

Et voilà ! En 35 lignes on obtient 80% d’utilité d’une bibliothèque et en plus on l’a comprise. Il s’agit de mon budget lorsque je passe une soirée là-dessus, comme j’ai pu le faire pour jQuery.

Il s’agit maintenant de le mettre en pratique. Considérons un compteur et une liste, on va avoir les actions pour ajouter et enlever une unité au compteur ainsi que l’ajout d’items à la liste (de courses). On commence par définir le nom de nos actions avec des constantes car c’est une bonne pratique et on en profite pour créer nos reducers :

const INCREMENT = 'INCREMENT'
const DECREMENT = 'DECREMENT'
const ADD_ITEM = 'ADD_ITEM'

const counter = createReducer({ counter: 0 }, {
  [INCREMENT](state, action) {
    return { counter: state.counter + 1 }
  },
  [DECREMENT](state, action) {
    return { counter: state.counter - 1 }
  }
})

const shoppingList = createReducer([], {
  [ADD_ITEM](state, action) {
    return [ ...state, action.payload.item ]
  }
})

Il faut bien faire attention à cette étape à bien retourner un nouvel état et non à modifier l’état courant sinon vous perdez complètement les avantages liés. L’étape suivante est de créer nos actions liées à ce que l’on vient de créer :

const increment = createAction(INCREMENT)
const decrement = createAction(DECREMENT)
const addItem = createAction(ADD_ITEM, 'item')

Ici on reste simple, seule la dernière function a un paramètre pour l’exemple. Il ne nous reste plus qu’à créer notre store à l’aide de la méthode précédemment définie :

const store = createStore((state = {}, action) => {
  return {
    counter: counter(state.counter, action),
    shoppingList: shoppingList(state.shoppingList, action)
  }
})

Il y aurait la possibilité de combiner les reducers mais je préfère rester explicite sur ce qu’il se passe dans mon store. Notre état va avoir deux clés (counter et shoppingList) qui restent indépendantes mais que l’on peut facilement parcourir à tout moment. Pour finir un exemple possible d’utilisation pour mieux comprendre :

console.group('Testing Redux concepts')
console.log('Begin:', store.getState())
const unsubscribe = store.subscribe((action) => {
  console.log('After', action.type, 'action:')
  console.table(store.getState())
})
store.dispatch(increment())
store.dispatch(increment())
store.dispatch(decrement())
store.dispatch(increment())
console.assert(store.getState().counter.counter == 2,
               'Counter is now equal to 2')
store.dispatch(addItem('Apples'))
store.dispatch(addItem('Oranges'))
console.assert(JSON.stringify(store.getState().shoppingList)
               == JSON.stringify(['Apples', 'Oranges']),
               'The shopping list has been filled')
unsubscribe() // Stop the logging subscription.
store.dispatch(decrement())
console.log('End:', store.getState())
console.groupEnd()

La seule subtilité est d’avoir le subscribe qui retourne la fonction permettant de se désinscrire une fois le moment venu. Ici ça ajoute un log dans la console avec l’état associé ce qui est pratique en debug (au passage, je découvre que l’on peut faire un console.assert). Mais qui s’avère également trivial si l’on souhaite le stocker/récupérer localement.

Tout ce qu’on rend téléchargeable au développeur, on l’empêche de l’inventer ou de le découvrir. Pour paraphraser Piaget qui lui-même paraphrasait Papert. Prendre le temps de réacquérir mes savoirs et mes concepts ne me permet pas d’être plus rapide ou meilleur mais je prends assurément plus de plaisir à les manipuler ensuite. Et lorsqu’on travaille sur des mandalas (cache), c’est la seule chose qui reste.

<troll>Oui, j’ai réussi à caser « plaisir » dans un billet sur JavaScript…