To React Natively with Context or Not

Photo by Hunter Harritt on Unsplash

The key mark of any experienced coder is one who is willing to refactor the code. Code deals in abstractions and generalizations at the high level, and with specifics at the low. If you imagine a ruler with abstract at one end and specific at the other, you might get a sense for the two endpoints. Thus, as your design comes to fruition, you begin to move pieces of code around on the abstract ← → specifics metric, adding additional abstraction layers to handle your cases.

My design was beautiful, my code flawless, my testing strategy without peer. There was nothing stopping me from finishing my code in time, and ensuring it met all my use cases.

I had one last case to handle — that of loading in some variables from state to ensure that the user could pick up where they left off.

The design choice was simple — I was already encapsulating the data that I needed to load in, properly passing it through to lower order components.

Component1 — called Component2 with a full Object, so that sub-components could utilize the fields of the Object. Component3 was called from Component2 with the values that it needed to display, not the full object. This was working great!

Right up until Component3 needed to load in and allow the user to change the values that the user had previous stored in higher components.

Values are stored on disk using a Single Key per Element, with a JSON blob of values for that Element. This choice was made because of 2 reasons:

  1. The number of Elements could be larger — although not more than a couple thousand.
  2. The number of keys in the JSON was low.

The good news is that the reasonable nature of these facts leaves us with a lot of flexibility — if necessary, we could store all values of all elements together — or we could break every key apart and store each JSON key separately.

Even more to our benefit, the values for each key can be programatically created by concatenating the Element ID with the JSON key name. This combination is guaranteed to be unique.

So, with that in mind, what’s the “best” way to solve this?

First, I decided to have Component2 attempt to load the values from the store and pass them down to Component3.

This *sounds* good in theory — the useEffect will fire once, when the component has mounted.

My component returns a value that renders another function. As part of its mount, it passes … the original passed in values… to the sub-component…

Only after this is displayed, does the useEffect fire… resulting in the values being read in from disk… and basically discarded. They never get passed down or anything.

I highly recommend the eslint-plugin-react-hooks being part of your setup:

Yeah. I got my hand slapped for that one.

The value is pretty much discarded.

So much for that approach.

Programming is the world of options though — and here we go:

  1. Have the lower level component, which is using the values… read them in from storage any time we needed them. This has the advantage that we always get the latest values. But the disadvantage that we have to wait for them to be read in from storage. Even if we don’t do this during a render function, I don’t like messing around with storage while formatting for display. It just adds latency that I don’t need. Once set by the user, the values rarely change, so there’s really no need to go to disk every time.
  2. Have Component2 add a loading screen, whereby it does not display Component3 until it has loaded the data from disk. This adds a small bit of Complexity, well, at least for my app, but allows us to get all the data we need up front. More complexity needs to happen on the backend though — if any of those values are again changed, say by Component3 →Component4 →Component5(DataField), and Component3 is again called on to display the value… it will display the last copy it saw, as it does not realize that the value changed. (To get a change notification to the data, we would have to pass a callback through multiple children to the child offering the ability to change the data.)
  3. Load the data inline in Component2 before passing the data down… This might work for my purposes — but makes me nervous — any storage slowness (like a full file system, or other active apps) is going to present to the user as a glitch in the system as the app takes a hiccup before displaying the first text. On the other hand, we should not have more than a MB of data, so modern flash systems should be able to handle that pretty easily. Note that this still has the same issue as #2 — we need to have a child component have a callback to change the data at a higher level.
  4. Use a React Context to load values into play, and store the values. In this case, a React Context would act like a global data store, allowing the app to set state directly in the React Context. It would greatly simplify the passing of callbacks, variables and other props from Component to Component. Another advantage of the Context API is that

So, looking at the options…

#1 is workable — but only from my smaller app perspective. If I were making a larger app with many, many values that persisted and could be changed, reading each and every one of the would be a problem.

#2 is workable — although passing around another callback function is annoying. Again, doable for a smaller app, could get extremely confusing for a larger app.

#3 is right out. Never load or process data inline with a render loop. Seriously people — what are we thinking here?

#4 this one has some advantages, but also brings some complexity to the table. It’s another feature of React that must be grokked. But, for global state management, it is essential. Note that global state management has the same issue as global variables — used sparingly, great! Too many global variables, and you’ve got a global mess. This is the reason why the React Context Docs suggest other methods of achieving this result without using Context. One advantage of the Context method is that any consumers registered as a user of the Context will get a render trigger when the Context itself is changed.

So, for this small app, I think I’m going to go with #4.

Context Relies on 2 things:

  • A Context Provider — or the component that is responsible for providing the context — this is usually the top of the tree.
  • A Context Consumer — one that will subscribe to and use the data in the context. (In functional Hooks — this is useContext.)

Let’s get going with that.

First, let’s start by defining our context store, which will default to no data.

const LocationContext = React.createContext();

With that in place, the next thing we need to do is to define a provider.

We’ll define the top of our locations tree as the context provider:

<LocationContext.Provider value=variables >
<Location
jsondata={route.params.story.locations}
locationID={route.params.locationID}
key={route.params.storyID}
variables={variables}
navigation={navigation}
storyID={route.params.storyID}
/>
</LocationContext.Provider>

Now we define the Consumer… since we are working with Functional Components, we’re going to use the ‘useContext’ api.

const keyText = useContext(LocationContext);

But…

Huh…. oh…. so — LocationContext is defined in Locations.js. VarInput.js is the file that is attempting to use the context.

To use the context, we can’t just create another one with the same name in VarInput.js. What we’ll need to do is reorg the code a bit — we’ll have to create a common file with the Context in it.

One file later, with the following 3 lines:

import React from ‘react’;const LocationContext = React.createContext();export {LocationContext};

And we can now import LocationContext from any file that needs to use the LocationContext, either as a provider or a consumer.

Adding to any file which is going to use the context:

import {LocationContext} from ‘../../classes/context.js’;

Now, in our Component3 — we see the following:

console.log(‘keyText: ‘ + keyText[variableIndex]);

And that results in….

keyText: hello

Whee! Maintaining context is fun!

What can we store in a context? Any JavaScript object. Put your Array, your json, anything.

Note though — ALL components will rerender whenever this value you’re storing changes. So, if you have data that is frequently changing, and your context stores a lot of it…. you’re going to have a lot of rerenders.

Consider for example:

  • Context 1 — Theme Info
  • Context 1 — Last Visited Page

Storing all of these in a single context means that every time the user visits a new page, any component (such as a side bar) registered to use Context 1 will rerender.

For performance reasons, you would be better off separating out the two Contexts into 2 separate contexts. Then, anytime the user navigates to a new page, you’d not have an extraneous rerender of components that are just using Theme info.

Storing a new value in the context is as easy as this:

keyText[variableIndex] = modalText;

So our final code to update the Location looks like this:

Pretty easy — now you have context!!!

Now, if anyone needs me… I’ll be refactoring my code.

Years of technology experience have given me a unique perspective on many things, including parenting, climate change, etc. Or maybe I’m just opinionated.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store