things i learned about the React ecosystem by

building without them

Wei / RK #64 / feb 28 2020

i've been building

a site generator

- packing

- serving

- creating the app

- routing

- a few critical functionalities and components

initial approach

striped off from someone else's existing project

 

along the way

re-examine each part and determine how exactly we want it to function

turns out our project is also

free of

class components & Redux

so far

today we're talking about a few of the problems I encountered thus far

1. document title

easy right?

document.title = "react knowledgeable"?

import * as React from 'react';

const useTitleNaive = title => {
  React.useEffect(() => {
    document.title = title;
  }, [title]);
};

export default () => {
  useTitleNaive('hello!');
  return <>title should be "hello!"</>;
};

why u no work?

react helmet

is managing the relevant tags

problem 1

setting title is a global side effect that persists

problem #2

Text

when multiple <Title /> components (or equivalent hooks) are mounted, it is hard to reason who wins

problem #3

import * as React from 'react';

const useTitleNaive = title => {
  React.useEffect(() => {
    document.title = title;
  }, [title]);
};

const Component = ({ title }) => {
  useTitleNaive(title);
  return <div>component sets the title to {title}</div>;
};

export default () => {
  useTitleNaive('page');
  return (
    <>
      page sets title to "page"
      <Component title="component" />
    </>
  );
};
import * as React from 'react';

const Title = () => {
  const useTitleNaive = title => {
  React.useEffect(() => {
    document.title = title;
  }, [title]);
};
}

const Component = ({ title }) => {
  return <div>
    <Title>{title}</Title>
    component sets the title to {title}</div>;
};

export default () => {
  return (
    <>
      <Title>Page</Title>
      page sets title to "page"
      <Component title="component" />
    </>
  );
};

how do other people do it?

another internal project

update title on every page

it's side effect with respect to React, but a feature in our app

how does

React Helmet

do it

we need to talk about React Side Effects

key concept

allows you to work on

all instances

of a component as a whole

so you can

determine who wins

when you have multiple <Title /> components

what happens under the hood

  • componentWillMount, componentDidUpdate: emitChange()
  • componentWillUnmount: remove the instance and emitChange()
  • emitChange():
    • calls reducePropsToState for you to turn relevant props to state
    • calls mapStateOnServer to execute side effects

recap

  • handles side effects from all instances of a component altogether
  • allows users to define a winning prop after iterating through all instances
  • acts in 2 phases
    • reduce props to state
    • execute side effects (client or server)
  • relies on class components' static properties

typical usages of

React Side Effects?

Dan Abramov:

React Document Title

solution we went for

arguably desired behavior for my project

> layout renders a “default” title, which individual pages and / or components can then overwrite

> leaving the page or unmounting the component “resets” the title back to a “clean state”

- <Title /> runs a side effect hook to set document title

- <Layout /> renders a <Title /> on every page with a default title, more nested <Title /> takes precedence

look back in this journey

- setting document title as a global side effects

- dealing with the side effects for a class of components together

- sometimes we may find easier solution because we have more assumptions about our projects

2. state management

regarding the huge battle

redux or not

  • we wanted to be un-opinionated
  • that actually means we build our core without reliant on Redux

page 1

page 2

page 3

user

app core

badges

app core

const useBadge = (id, text) => {
  const { badges, setBadges } = useContext(BadgeContext);
  useEffect(() => {
    setBadges({...badges, [id]: text});
  }, [id, text])
};


const MyPage = () => {
  useBadge('weather', 'raining');
  useBadge('notification', '2');
  return (<>
    <Badge id="weather" />
    <Badge id="notification" />
  </>);
};

the 2nd setBadge call will overwrite the result of the 1st call with the old state with the 2nd new badge only

use useReducer to decouple action with update logic

const reducer = (state, action) {
  switch (action.type) {
    case 'new badge': {
      return {
        ...state,
        [action.id]: [action.text]
      };
    }
    default: {
      throw new Error();
    }
  }
}

const MyPage = () => {
  const [, dispatch] = useReducer({});
  dispatch({type: 'new badge', id: 'weather', text: 'rainy' });
  dispatch({type: 'new badge', id: 'notification', text: '2' });
  return <>
    <Badge id="weather" />
    <Badge id="notification" />
  </>;
}

some pseudocode

badges

app core

user

now we have multiple states live inside separate contexts, each managed by `useReducer`

badges

app core

user

sider collapse state

current route and params

?

event system

dispatch / subscribe with payload

we use dispatched events with payloads as a bridge for communication

// core library

const Layout = () => {
  useEffect(() => {
    return subscribe('debug', ({page}) => { 
      console.log(`debugging ${page}`) 
    });
  }, []);
  return // route-based page loader
};
// page
import { dispatch } from 'outerspace-headquarter';

const MyPage = () => {
  dispatch('debug', { page: 'my page' })
  return <div />
};

each communication pathway connects an interaction to a callback, but most of the time, the callback sets payload to an internal state

- multiple stateful variables w/ useReducer

- event system

maybe we should just bring Redux back

someone created an issue:

persist data between pages

the request goes

“can we have something similar to <keep-alive /> of vue.js”?

“Keep data cached separately from the component. For example, you can lift state up to an ancestor that doesn't get mounted, or put it in a sideways cache like Redux.”

- Dan's comment

changing part of the store does not change other part... kind of serves as a temporary storage cross the SPA

thoughts around <keep-alive />:

  • Hoist the data to a parent component
    • not practical cuz props drilling
  • Via state management such as Redux (next slide)
  • Maintain data in a separate stateful variable, put it in context, and access with hooks
  • Put data somewhere else and retrieve with hooks
    • flexible storage of data

is Redux “too heavy”?

- redux is tiny

- react redux not big neither

- the work needed to make Redux work with our apps

routing, code splitting, SSR...

Redux-y things we do anyway

- individual reducers in context

- events

Redux-y things we miss

- separate data states with app states

but, we may not want to bring back Redux because we don't want our app to be so centralized

page 1

page 2

page 3

listing

detail

edit

page 1

page 2

page 3

user

routes

ui states

app core

3. loading + navigating

flash of loading component

React Loadable:

wait a short amount of time before displaying the loader component

but it will still flash a white screen when `null` is being rendered

so how ah

const App = () => (
  <main>
    <Router>
      <Loader path="/cat" key="/cat" />
      <Loader path="/dog" key="/dog" />
    </Router>
  </main>
)


const loaderMap = {
  "/cat": () => import("./cat"),
  "/dog": () => import("./dog")
};
const Loader = ({
  path,
  loader,
  children
}) => {
  const [module, setModule] = React.useState(modules.get(loader));
  React.useEffect(() => {
    if (!module) {
      fetchModule(path, loader).then(mod => {
        setModule(mod);
      });
    }
  }, [loader, path, module, setModule]);

  if (!module) {
    // this _will_ result in a flickering before the 1st time the component is loaded
    return null;
  }

  const { default: Component, ...rest } = module;

  return <Component {...rest}>{children}</Component>;
};

still ffffflickering...

arguably more finely controlled behavior: preload component before navigating

> preload / prefetch is more of navigating logic than of loading logic

> so loaders aren't really supposed to take care of this... (at least not yet, with the React ecosystem)

const fetchThenNavigate = path => e => {
  e.preventDefault();
  fetchModule(path).then(() => navigate(path));
};
const fetchModule = async (path, customLoader) => {
  if (modules.get(path)) {
    console.info(
      `%cModule ${path} already loaded`,
      `color: #82aaff;${commonLogStyles}`
    );
    return modules.get(path);
  }

  const loader = customLoader ? customLoader : window.loaderMap[path];

  if (!loader) {
    throw new Error(`Didn't see a loader`);
  }

  console.log(`%cLoading ${path}`, `color: #addb67;${commonLogStyles}`);
  return loader()
    .then((module: any) => {
      console.info(`%c${path} loaded`, `color: #22da6e;${commonLogStyles}`);
      modules.set(path, module);
      return module;
    })
    .catch((err: string) => {
      console.error(err);
    });
};

> not to confuse with html's prefetch / preload attribute though

> PRPL Pattern is also a thing

you can use a loader fn that returns a promise, and wait for that promise to fulfill before you navigate

React Loadable provides this function, but says it creates bad user experience

to create the wheel...

  • we have more assumptions to leverage on so we can stick to simpler solutions
  • we have special requirements that are not addressed by existing libraries

or not create

  • we end up solving the same problems
  • we come down to canonical solution

such a big topic, i'm scared of Q&As

things i learned about the react ecosystem by building without them

By Wei Gao

things i learned about the react ecosystem by building without them

  • 671