Redux Normalized data update
De normalizr
library wordt vaak gebruikt om geneste data om te zetten in een genormaliseerde vorm om het te integreren in de store.
Maar met normalizr
zelf kan je geen updates bewerkstelligen van data in je applicatie die verder niet genormaliseerd verwerkt wordt.
Er zijn verschillende benaderingen van dit probleem mogelijk. Dit is erg afhankelijk van je eigen voorkeur. Aan de hand van een voorbeeld van het bewerken van mutaties van Comments op een Post zal ik het proberen uit te leggen.
Standaard aanpak
Simpel samenvoegen
Een van de manieren van aanpak is om de inhoud van de actie samen te voegen met de bestaande status.
Bijvoorbeeld door een deep recursive merge (dus geen shallow copy) te doen, zodat het mogelijk is om gedeeltelijke aanpassingen op te slaan. De lodash
merge functie kan dit voor ons bewerkstelligen:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import merge from "lodash/merge"; function commentsById(state = {}, action) { switch (action.type) { default: { if (action.entities && action.entities.comments) { return merge({}, state, action.entities.comments.byId); } return state; } } } |
Deze methode vergt het minste werk aan de reducer kant, maar vereist wel dat de action-creator in staat is om de data in de vereiste vorm brengt voordat de action ge-dispatched wordt. Het is bovendien lastig om op deze manier om een item te deleten.
Slice Reducer manier
Als we geneste slice-reducers hebben, zal elke slice-reducer willen weten hoe het moet correct moet reageren op elke action. We zullen alle relevante data mee moeten geven in de action. We moeten het juiste Post object updaten met de comment’s ID, een nieuwe Comment-object creeren met dat ID als key, en het Comment’s ID toevoegen aan de lijst van alle Comment ID’s. Hieronder zie je hoe dat allemaal samenkomt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// actions.js function addComment(postID, commentText) { // Genereer een unique ID voor deze comment const commentID = generateID("comment"); return { type: "ADD_COMMENT", payload: { postID, commentID, commentText } }; } |
In de action function hierboven stellen we de opdracht samen voor de twee volgende (intelligente) reducers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// reducers/posts.js function addComment(state, action) { const { payload } = action const { postID, commentID } = payload // Zoek de juiste post bij de ID om de rest van de code simpel te houden const post = state[postId] return { ...state, action // Update ons Post object met een nieuwe 'comments' array [postID]: { ...post, comments: post.comments.concat(commentID) } } } function postsById(state = {}, action) { switch (action.type) { case 'ADD_COMMENT': return addComment(state, action) default: return state; } } function allPosts(state = [], action) { // niet ingevuld, niet nodig voor dit voorbeeld } const postReducer = combineReducers({ byId: postsById, allIds: allPosts }) |
en
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// reducers/comments.js function addCommentEntry(state, action) { const { payload } = action const { commentId, commentText } = payload // Maak ons nieuwe Comment object const comment = { id: commentId, text: commentText } // Voeg het nieuwe comment object toe aan de geupdate opzoektabel: return { ...state, [commentId]: comment } } function commentsById(state= {}, action) { swithc (action.type) { case 'ADD_COMMENT': return addCommentEntry(state, action) default: return state } } function addCommentId(state, action) { const { payload } = action const { commentId } = payload // Voeg de nieuwe Comment ID toe aan de lijst van alle ID's return state.concat(commentId) } function allComments(state = [], action) { switch (action.type) { case 'ADD_COMMENT': return: addCommentId(state, action) default: return state } } const commentsReducer = combineReducers({ byId: commentsById, allIds: allComments }) |
Dit is best een lang voorbeeld omdat het alle slice-reducers en case reducers in een keer laat zien en hoe ze bij elkaar passen.
Let goed op de uitbesteding van de taken:
-
De
postById
slice reducer besteedt het werk in dit geval uit aanaddComment
, die de nieuwe Comment ID toevoegt aan de juiste Post. -
In de tussentijd hebben zowel
commentsById
alsallComments
slice reducers hebben hun eigen case reducers, die de Comments opzoektabel en de lijst van Comment ID’s naar behoren bijwerken
Andere aanpak
Taak gebaseerde updates
Omdat reducers gewoon functies zijn, is er een oneindige hoeveelheid manieren waarop je de logica kan indelen. Hoewel het gebruik van slice-reducers
hiervoor het meest gebruikelijk is, is het ook mogelijk om het verwerken op een wat meer taak-gerichte manier te organiseren.
Omdat dit vaak meer ‘nested’ updates betreft, is het voor de hand liggend dat je gebruik wil maken van een immutable update library zoals dot-prop-immutable
of object-path-immutable
, om de updatestatements gemakkelijker te houden.
Hieronder volgt een voorbeeld van hoe dat er uit zou kunnen zien:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
import posts from "./postsReducer"; import comments from "./commentsReducer"; import dotProp from "dot-prop-immutable"; import {combineReducers} from "redux"; import reduceReducers from "reduce-reducers"; const combineReducers = combineReducers({ posts, comments }) function addComment(state, action) { const { payload } = action; const { postId, commentId, commentText } = payload; // de State is hier de complete gecombindeerde State const updateWithPostState = dotProp.set( state, `posts.byId.${postID}.comments`, comments => comments.concat(commentId) ); const updatedWithCommentsTable = dotProp.set( updatedWithPostState, `comments.byId.${commentId}`, {id: commentId, text: commentText} ); const uopdatedWithCommentsList = dotProp.set( updatedWithCommentsTable, `comments.allIds`, allIds => allIds.concat(commentId); ); return updatedWithCommentsList } const featureReducers = createReducer({}, { ADD_COMMENT: addComment, }) const rootReducer = reduceReducers( combinedReducer, featureReducers ) |
Deze benadering laat het heel duidelijk wat er gebeurd in de "ADD_COMMENTS"
situatie, maar het vergt wel nested update logica, en behoorlijk wat kennis van de state tree shape
. Afhankelijk van hoe je je reducer logica wilt bouwen, is dit wel of niet wenselijk.