← All Posts

Handling a Sidebar with Redux

redux |

These days, I started learning Redux and tried to integrate it in an existing project. The project is public, kemos.care on Github The initial train of thought was easy : identify which part of my app was a state element and isolate it into reducers and actions. This article will be a walkthrough on how I integrated Redux in my app. I chose a Vanilla and unopiniated redux and did not go with Redux Toolkit.

I didn't realize at that time that state was a huge part of a React app. JSX makes React apps looks like plain markup documents, the API for state handling (eg. setState) does not just scale for big apps that have deeply nested components.

The app is setup in 3 simple and straigtforward parts. Let's start with the Sidebar.

app-wireframe

Sidebar

Well, from the basic tutorial, it says you should start with actions. On the tree itself, I identified 3 actions and modeled them like this :

  • EXPAND_NODE for expanding a node when it contains child nodes
  • COLLAPSE_NODE : the opposite
  • SELECT_NODE for bottom-level nodes that triggers other interface updates.

And a special one :

  • FILTER_TREEE that would trigger every time a key was stroke, kind of.

Node structure

A simple node in my tree had to look like this in order to work with the UI library components I used :

{
    id: <ObjectId>
    name: "",
    childNodes: [],
    isSelected: false
}

The click event on a node returns the node's id, I then recursively ran down the tree to deselect any protocols that did not match the selected id. When the id was match with a node in the tree, it mutated that node by setting the isSelected: true property.

It then mutated the selected node by setting isSelected: true. The problem there is that the contentTree is directly modified, not "redux-y", needs a rewrite.

To benefit from everything redux had to offer, I needed to, on each click, return a whole new tree, with a single pass over the new tree. So I went on and wrote a reducer that would take a node as a function that would have those 3 actions.

Handling recursivity with forEachNode(nodes, callback)

The reducer for my node state is dead simple. It takes the node that needs changing and a simple switch over my 3 possible would make it return a new changed node. Recursively going through all nodes recursively was

function forEachNode(nodes, action) {
    return nodes.map(node => {
        if(node.childNodes === null) { //deepest element of the tree
            return node(node, action)
        } else {
            /* returns a new node treated with the reducer, childNodes being treated recursively */
            return node({
                ...node, //the previous node's values layed out into a new node
                childNodes: forEachNode(node.childNodes, action) //an overriden childNodes array with the recursive `forEachNode` function
            }, action)
        }
    }
}

Filtering

The FILTER_TREE action was a bit tricky. It is at the action level that Redux lets you handle timeOuts and such.

The issue is, if a user types fast, It triggers too many filtering actions and rerenders that are not pertinent. In my initial implementation, I had set up a timeOut function in order not to trigger a filter action every time a key was pressed. It was straightforward, I had a timeOut property in my component that I could keep track of. With actions and reducers, my components became decoupled from their state.

The function looks like this :

let timeout = "" //sets a top-level global variable containing a timeout value
export function keyTypedFilterTree(query) {

    return function(dispatch) { //dispatches a function as thunkMiddleware makes possible

        dispatch(keyTyped(query)) //a keyTyped action is dispatched containing the whole text
        clearTimeout(timeout) //invalidates the timeOut each time a new key is typed
        //creates a new timeout that will eventually dispatch our `FILTER_TREE` action
        timeout = setTimeout(() => dispatch(filterTree(query)), 500) 
        return timeout
    }
}

Issue with filtering while typing. Two things

  • My forEachNodes func is no longer adapted because it maps the tree instead of filtering it.
  • I need to find a way to make that timeOut work.

As for filtering, nodes.filter can not work because I need to return modified new nodes, while filter returns new node based on the existing ones, not changeable. I could do something like node.map().filter() but that would mean a two pass on a single array. My opinion is that it's in these kind of situation that for loop are useful.

For timeOut, let's use the thunkMiddleware. The definition of the thunk middleware is that it allows you to dispatch a function instead of an action. My mental model is that it allows to dispatch multiple actions with it's own logic (but no side effect on state, because redux). The way I found here is something seen in the redux tutorial although I don't think it is very redux-y. I use the thunk to dispatch a function that triggers two actions :

  • One that registers a new key press, allowing for the whole tree to collapse when a new key is typed.
  • One that is launched after a setTimeout of 500ms to actually filter out the tree and expand the filtered items.
  • Of course, if a new key is typed before the delay triggers the action, it cancels it. """ demo """

It all seems to work after some debugging (many "," or ":" mixed up). Let's now delete all the old state-related code off or Sidebar component. (satisfying)

Okay, I now have a code that is not only refactored but that has new functionnalities like time travelling debugging. My reducer is over 200 lines of codes (many with { or [ or ...state ) but I feel it is easier to read for anyone that understands the redux mental model.

Forms with Redux

Okay, it is one those shiny moments, I've been struggling in my mind with forms for a few days not knowing where to start. So one of the main component of my app is a form that is used to add new chemotherapy protocol. It needs to get all the specificities and thus the form is changeable and one can add or remove elements from it.

Component rewrite

The way my components were built did not allow an easy redux integration. This is why I could not wrap my head around it. Unlike the Sidebar, where the component was already written (Blueprint Tree), my components were using javascript classes and most of them manipulated the state from upper components but also had some state of their own. Like the number of line the subform contained. Integrating redux means moving the state away from components, this allows a rewrite in the form of presentational component (link).

const ProductForm = (product, dispatch) => (
    ...
)

My ProductForm subform component will now be loaded from a product, and modifications will use the redux function dispatch to dispatch one of three : inputChanged, radioChanged, productRemoved, productAdded.

← All Posts