A different approach to data-sharing of server data/lists across components.

 

I ran into a new challenge when I was refactoring and organizing code and components into smaller reusable pieces.  I ran into a case where HOC-like approaches wont work, but perhaps an Inheritance model will, and settled on a much different approach.

Let’s discuss the use case.  Imagine your server holds a list of People records.  You create your basic React “list” component to show perhaps just the last names.  This component might be used in a few places in your app so it is nice that it is reusable

The component does the expected Comm-I/O in a useEffect() (or componentDidMount()) and you use your typical “{list.map(…..)}” for render-expansion.  All is good.

 Suppose in one use-case, the UI wants to show the list AND allow a text-input to “add a new Person” to the list.  You *could* add a prop boolean such as “props.allowNewSubmission” but looking ahead you worry that you could add all sorts of behavior props which could get ugly.  What you really want to do is create a wrapper component that invokes the list component AND adds-on an <input>. Each enough, BUT, the comm-I/O code is inside the list component; plus, when you add a new Person, the list component (everywhere it currently showing) needs to know to re-fetch and/or update.

 You can’t do this easily with HOC or similar approaches.  If you subclass the list component, then you have access to the comm-I/O functions plus perhaps a “re-fetch()” method that will trigger the necessary state changes for a re-render.  This would work for this case, but what if you then wanted another wrapper for another feature OR if you had the list panel showing in more than one area of the screen at the same time?

 Our Front-end world (and particularly with React) we tend to combine our “show server data” components WITH the actual comm/business logic.  Works most of the time, simple-enough, and keeps code relatively clean. But if that server-side data needs to be used and shared by more-than-one-component it becomes a problem.

So, let’s introduce a regular OOP style and create a List class.  This class will offer a few methods for getting and setting and deleting and other REST accesses to the server.  But this class will be a bit different and somewhat of a hybrid singleton+instance. What this means is that each user of the class will still “new()” it as they would expect, but the core data list (and some other management data) will be module-static. Why? Basically as a cache–all super fast “getList()” functions (which the UI re-render calls frequently) to be fast plus fast if cross-shared across components.  This is good, but what about updates? Any and all component-users of this class need to know if changes took place (so that they can re-render correctly). So, this class will sport a “subscribe()” and “unsubscribe()” function. In fact, when the user-component does the ‘new’ it can provide the subscribe handler saving a step (note: it is not necessary to subscribe but you’ll miss updates from other component uses, which might be OK some of the time).

Now need to tie the activity of an update to actually causing a component (all subscribed components) to re-render.   In our list-using component, we create a useState() of any arbitrary piece of data simply so that any set-state of the data forces a re-render.  Now, we subscribe to the list component, and for the subscribe-call-back, we pass-in the set-state of that dummy variable. Thus, when any system activity calls into the List class and causes a change (addition of an item, deletion, re-ordering, etc) the List handler will invoke all the subscribers and in this scenario calls the set-update which then triggers each component to re-render.  So simple, so clean, and so centralized.

 Here is the entire ShowList.jsx component:

const ShowList = props => {

 const [update, setUpdate] = useState(0);

 const [list] = useState(new List(setUpdate));

 useEffect(() => {

   return list.unsubscribe;

 }, []);

 if (!list.getList()) {

   list.fetchFromServer();

   return

Awaiting…

;

 }

 return (

My List

     {list.getList().map(item => {

       return

{item.name}

;

     })}

     <button

       onClick={e => {

         list.pushItem({ name: “joe”, id: new Date() });

       }}

     >

       Add

     

 );

};

 

Very clean and simple.  Our component can use the “list” instance object anywhere it likes, usually grabbing the actual list-data, sometimes changing or adding or deleting items.  Any changes (which must go through the List class) will trigger the subscribers and thus the re-rendering. 

Now, let’s take a look at a sample List class.  Note, this one does NOT do any comm I/O, but clearly it could.

let _list = null; // Single instance of the list for all subscribers

let _subscribers = {}; // List of subcribers to notify when changes occur

let _counter = 0; // Used per-instance to create a unique subscriber name

let _updates = 0; // Sent on every subscribe-udpate so that they have a new value

 

class List {

constructor(subFunc) {

this._counter = _counter++; // So that each instance has a unique name/key

if (subFunc) this.subscribe(subFunc);

}

 

getList() {

// Return a cached single-instance copy for all subscribers

return _list;

}

 

subscribe = func => {

_subscribers[this._counter] = func;

};

 

unsubscribe = who => {

delete _subscribers[this._counter];

};

 

fetchFromServer = () => {

// Clearly, this would ask the server/fetch() for data

return Promise.resolve().then(() => {

_list = [

{ name: “chris”, id: 50 },

{ name: “joe”, id: 59 }

];

this._ping();

});

};

 

pushItem = item => {

// Clearly, this would send and update/post to the server

// and a re-fetch

_list.push(item);

this._ping();

};

   _ping = () => {

_updates++;

for (let subs in _subscribers) {

_subscribers[subs](_updates);

}

};

}

You can see the places where it could perform real server I/O.  There is a subscribe function that the caller can call, or the caller can pass-in the subscribe call-back to the constructor.  The unsubscribe() MUST be called and in our use-case above we call that during the component unmount (via the useEffect method).

You’ll see that the cached-list is a module global so that it is shared like a singleton, as is the list of subscribers.  Similarly there is a global ‘counter’ that is up-ticked on every “new()” and this helps build a unique key for the subscription list (we DON’T want the caller to pass-in a key, such as “PeopleList” because if that component is rendered in multiple panels at the same time the subscriptions would not be unique).  And there is another counter that monotonically upticks every time a subscription notification is run; why? Because this value is passed to the subscriber which in our case is then passed to the hook’s “set” function which will ONLY cause a re-render if the newly-set value is different from the last one set.

Anyway, does this help with my original challenge, where I tried to subclass my Person component so that I could add a “Add person” field?  Yes. I can now create a component that invokes the Person component and adds to it a <text> input; this upper component will also “import” “List.js” and when it call’s list’s “push” (or add, etc) method, the server will get the transactions and all subscribers will be notified and all panels updated.

Cool

1 thought on “A different approach to data-sharing of server data/lists across components.”

Leave a Reply to Carol Maloney Cancel Reply

Your email address will not be published. Required fields are marked *