DEV Community

joon
joon

Posted on

My React takeaways (2020 April~August)

Multi-API fetching React project structure with profuse usage of Redux

Alt Text
The original outline

The site is not finished yet, with a lot of tedious work left consisting of parsing and displaying blockchain transactions, but the general structure which I had envisioned at the beginning of working on the project is pretty much visible, plausible and most importantly criticizable.
Here are the dos, donts, trys and takeaways. So this was the original intent of writing this, but it just ended being a bunch of random tips and takeaways that I amassed while working on this project.

Do - learn how to use RxJS.

But only after you realize that RxJS is truly a god-send in writing readable, maintainable, concise multi-asynchronous code. The project I was working on was I believe to be a really rare specimen of a project, where using RxJS could drastically improve DX(Developer Experience). Simple projects where asynchronousity is not too abundant should work wonders with fine-tuned custom hooks to suit your needs. I admit that I probably haven't even scraped the surface of the true power and paradigm-shifting utility of RxJS, but a bare understanding and skim through the operators was enough for me to realize that learning RxJS was like learning React after just using javascript.

Try - Managing all APIs, routes and retrieved data refining functions in a separate location/file

const refiner = {  
  default: new Map(),  
  ...getInitStatePerChain(new Map()),  
};  

_.each(_.keys(chains), v => (refiner[v] = new Map()));  
const {  
  REDUX: {PERIODICS, DATA},  
  DB,  
} = consts;  

//  /status  
const statusProps = ["block_height", "block_time", "total_validator_num", "unjailed_validator_num", "total_supply_tokens", "bonded_tokens", "total_txs_num"];  
refiner.default.set(PERIODICS[0].name, data => {
    //  pick values and refine
}

//  .....
//  a lot of refiners
//  ....


export const apiRefiner = (chain, name, data) => {  
  if (!_.isMap(refiner[chain])) throw new Error(`CRITICAL - chain ${chain}'s refiner is non-existent`);  
  const refinerFunc = refiner[chain].has(name) ? refiner[chain].get(name) : refiner.default.get(name);  
  if (!_.isFunction(refinerFunc)) throw new Error(`CRITICAL - default refiner function for ${name} is non-existent`);  
  return refinerFunc(data);  
};
Enter fullscreen mode Exit fullscreen mode

If I had to pick the one thing that I tried out that probably increased the most productivity, it was this.
Manage all routes in a config file, manage all data refining by defining a map, and mapping the route to a refining function which is used to refine all data retrieved from said refiner. This method has several pros and minimal cons.

  • Pro - Using the same API in multiple places couldn't be easier(Modularizing the whole thing and calling the API using a custom hook turned the whole process into a single line of code + defining values in the config)
  • Pro - No need to use postman or any other API displaying tools(mostly), it's all nicely tucked inside your refiner file
  • Pro - API related logic apart from using the data gets completely detached from your components/containers
  • Con - initial setup takes some time

Actually using it would look something like this in a custom hook

React.useEffect(() => {  
  if (empty(key) || timestamp !== 0) return;  
  const targetUrl = `${consts.ENV[chain].api}${dbObj.route(key)}`;  
  simpleGet(targetUrl)  
      .then(res => {  
          // console.log(res);  
          sort(res.data).desc(v => v[idKey]);  
          const refinedData = apiRefiner(chain, dbObj.name, res.data);  //  refiner
          setData(refinedData);  
          const docs = _.map(refinedData, v => makeDoc(v, v[idKey]));  
          db.lazyBulkPut(docs);
      })  
      .catch(ex => {  
      console.warn(`error during fetchToDB - ${targetUrl}`, ex.message);  
      setError(true);  
      });  
}, [key, setData, setError, data, chain, timestamp, db]);
Enter fullscreen mode Exit fullscreen mode

which leads to the next takeaway.

Hooks are awesome. I recommend migrating and never looking back.

The one thing that I found inconvenient was not having a componentShouldUpdate and ErrorBoundaries.
But after getting used to the 'proper way' of using useEffect(which I frequently stray from btw), and almost compulsively wrapping everything that I can get my hands on with useCallback or useMemo, missing componentShouldUpdate became pretty much trivial. As for ErrorBoundaries... well let's leave that as a con :)

Do - take care of object reference equality & in-equality

So a certain component was rerendering way too many times even before it had any kind of value. Turned out I had given it a default value of {} which was initialized somewhere else, resulting in a new object every time, thus causing the rerender. Object reference in-equality.
Ever since that experience, I created the following constant in my consts.js file, and in scenarios where initializing to an object was needed, I simply used that instead.

consts = {
//  ...
MISC: {  
  DEFAULT_OBJ: {},  
  DEFAULT_ARR: [],
  }
//  ...
}
Enter fullscreen mode Exit fullscreen mode

Do use Reselect

On a heavily populated, memoized component with a useSelector hook, try console.counting inside the useSelector.
In my case, I saw the number shoot up to 80 before any content appeared to be painted which forced me to learn reselect. It took its toll, but to no regrets. I realized it was pretty much the 'memoized refiners' for selectors.
One thing that I felt was somewhat uncomfortable was that it was recommended in the reselect docs to pass parameters to reselect via redux, forcing me to create a new store that just handled variables I needed to pass. But still worth the hassle.

A neat real-time wrapper component

import React from "react"; 
import {interval} from "rxjs";

const source = interval(1000);
export default function RealTime({value, generator = () => null}) {  
  const [, updateState] = React.useState(true);  
  const forceUpdate = React.useCallback(() => updateState(v => !v), []);  

  React.useEffect(() => {  
  const subscribe = source.subscribe(() => forceUpdate());  
  return () => subscribe.unsubscribe();  
  }, []);  
  return <>{generator(value)}</>;  
}
Enter fullscreen mode Exit fullscreen mode

Another reason to use RxJS. Make the generator do some kind of calculation that changes in time, and voila, you have a component that is real-time, and will be in sync with all other real-time wrapped components.

Switch css with js instead of React if possible and plausible

export default function () {  
  document.documentElement.style.setProperty("--bgHeaderColor", "linear-gradient(to right, #272538, #35305e 81%)");  
  document.documentElement.style.setProperty("--chainNameColor", "#ffffff");  
  document.documentElement.style.setProperty("--color-main", "#9c6cff");  
  document.documentElement.style.setProperty("--bgDashImage", "var(--bgDashCosmos)");
  //    ...
}
Enter fullscreen mode Exit fullscreen mode

Define variables in your scss / css and change the values using javascript to switch themes / change appearances. Memoize the jsx and you've saved a rerender from repainting the entire component.(Memoizing with accurate dependencies works as well, but this method is so much more straight-forward and easy)
Example in React

const [smallSearch, setSmallSearch] = React.useState(false);  
React.useEffect(() => {  
  if (smallSearch) document.documentElement.style.setProperty("--searchHeight", "47px");  
  else document.documentElement.style.setProperty("--searchHeight", "0px");  
}, [smallSearch]);
Enter fullscreen mode Exit fullscreen mode

AFAIK, this method takes much less energy than removing and adding elements to the dom.

Dont - over-cache data

Apart from extreme scenarios - which I thought mine was - high-intensity data caching takes too much time compared to the productiveness of the output. Since I was handling blockchain data which basically means fetched chain data will pretty much never change, it felt perfectly sane to try shoving every bit of data in a DB and checking and retrieving the data from the DB if it existed before attempting to fetch.
I believe the costs far out-weighed the results. It is ultra-difficult, forcing you to build different custom hooks for all kinds of asynchronous scenarios. In hindsight, it might have been worth it if I was well versed in RxJS, but attempting the feat with mostly custom hooks was not my Sunday cup of tea.

Hopefully, this helps someone. Cheers.

Oldest comments (0)