React hooks clone and related concepts
Getting Closure on Hooks presented by @swyx
// https://www.youtube.com/watch?v=KJP1E-Y-xyo
const React = (function() {
let hooks = [];
let idx = 0;
function useState(initVal) {
const state = hooks[idx] || initVal;
const _idx = idx;
const setState = newVal => {
hooks[_idx] = newVal;
};
idx++;
return [state, setState];
}
function useEffect(cb, depArray) {
const oldDeps = hooks[idx];
let hasChanged = true;
if(oldDeps) {
hasChanged = depArray.some((dep, i) => !Object.is(dep, oldDeps[i]));
}
if(hasChanged) cb();
hooks[idx] = depArray;
idx++;
}
function render(Component) {
idx = 0;
const C = Component();
C.render();
return C;
}
return { useState, useEffect, render };
})();
function Component() {
const [count, setCount] = React.useState(1);
const [text, setText] = React.useState('apple');
React.useEffect(() => {
console.log('useEffect with count dep')
}, [count]);
React.useEffect(() => {
console.log('useEffect empty dep')
}, []);
React.useEffect(() => {
console.log('useEffect no dep')
});
return {
render: () => console.log({count, text}),
click: () => setCount(count + 1),
type: word => setText(word)
}
}
var App = React.render(Component);
App.click();
var App = React.render(Component);
App.type('pear');
var App = React.render(Component);
/*
useEffect with count dep
useEffect empty dep
useEffect no dep
{count: 1, text: "apple"}
useEffect with count dep
useEffect no dep
{count: 2, text: "apple"}
useEffect no dep
{count: 2, text: "pear"}`
*/
Vanilla React data fetching
If you’re going to fetch in useEffect()
, you should at least make sure that you’re handling:
- Loading states
- Error handling (rejections & HTTP error codes)
- Race conditions & cancellation
import * as React from "react"
export default function useQuery(url) {
const [data, setData] = React.useState(null)
const [isLoading, setIsLoading] = React.useState(true)
const [error, setError] = React.useState(null)
React.useEffect(() => {
let ignore = false // isCancelled
const handleFetch = async () => {
setData(null)
setIsLoading(true)
setError(null)
try {
const res = await fetch(url)
if (ignore) {
return
}
if (res.ok === false) {
throw new Error(`A network error occurred.`)
}
const json = await res.json()
setData(json)
setIsLoading(false)
} catch (e) {
setError(e.message)
setIsLoading(false)
}
}
handleFetch()
return () => {
ignore = true
}
}, [url])
return { data, isLoading, error }
}
In reality, we still need to think about:
- For every component that needs the same data, we have to refetch it.
- It’s possible that while fetching to the same endpoint, one request could fail while the other succeeds.
- If our state is moved to “global”, we’ve just introduced a small, in-memory cache. Since we’ve introduced a cache, we also need to introduce a way to invalidate it.
- Context often becomes confusing over time. A component subscribed to QueryContext will re-render whenever anything changes – even if the change isn’t related to the url it cares about.
- We’re treating asynchronous state as if it were synchronous state.
That’s why React Query was created.
You Might Not Need an Effect
Whenever you think of writing
useEffect
, the only sane thing is to NOT do it. Instead, go to the react docs and re-read the page about why you don’t need an effect. You really don’t. -@TkDodo
When developing an application in React 18+, you may encounter an issue where the useEffect
hook is being run twice on mount. This occurs because since React 18, when you are in development, your application is being run in StrictMode by default. In Strict Mode, React will try to simulate the behavior of mounting, unmounting, and remounting a component to help developers uncover bugs during testing. From the user’s perspective, visiting a page shouldn’t be different from visiting it, clicking a link, and then pressing Back. React verifies that your components don’t break this principle by remounting them once in development. In most cases, it should be fine to leave your code as-is, since the useEffect
will only run once in production.
- https://react.dev/learn/you-might-not-need-an-effect
- https://eslint-react.xyz/docs/rules/hooks-extra-no-direct-set-state-in-use-effect
- https://www.youtube.com/watch?v=bGzanfKVFeU
Referencing Values with Refs
When you want a component to “remember” some information, but you don’t want that information to trigger new renders, you can use a ref
. Typically, you will use a ref when your component needs to “step outside” React and communicate with external APIs. (e.g. storing timeout IDs, DOM elements)
- Refs are an escape hatch to hold onto values that aren’t used for rendering. You won’t need them often.
- A ref is a plain JavaScript object with a single property called
current
, which you can read or set. - You can ask React to give you a ref by calling the
useRef
Hook. - Like state, refs let you retain information between re-renders of a component.
- Unlike state, setting the ref’s current value does not trigger a re-render.
ref
callback function
Instead of a ref object, you may pass a function to the ref attribute. When the <div>
DOM node is added to the screen, React will call your ref
callback with the DOM node as the argument. When that <div>
DOM node is removed, React will call your ref
callback with null. React will also call your ref
callback whenever you pass a different ref
callback.
const scroller = (node: HTMLDivElement | null) => {
node?.scrollIntoView({ behavior: "smooth" });
};
const ChatWindow = () => {
return (
<>
{Array.from(Array(100).keys()).map((e) => (
<div key={e}>Chat message: {e}</div>
))}
<div ref={scroller} />
</>
);
};
So if you need to interact with DOM nodes directly after they rendered, try not to jump to useRef
+ useEffect
directly, but consider using callback refs instead.
Higher Order Components
HOCs are wrapper components that help provide additional functionality to existing components. While hooks probably replaced most of shared logic concerns, there are still use cases where higher-order components could be useful. For example, you want to fire analytics event on every click of every button, dropdown and link everywhere.
export const withLoggingOnClick = (Component) => {
return (props) => {
const log = useLoggingFromSomewhere();
const onClick = () => {
// console.info('Log on click something');
log('Log on click something');
props.onClick();
};
// return original component with all the props
// and overriding onClick with our own callback
return <Component {...props} onClick={onClick} />;
};
};
What’s new in React 17+
React 17:
React 18:
- https://react.dev/blog/2022/03/29/react-v18
- https://www.youtube.com/watch?v=Z-NCLePa2x8
- https://www.youtube.com/watch?v=ytudH8je5ko
React 19: