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