Working with rapid changing styles
React is fast. Very fast. It's very rare for me to run into performance issues assuming I follow best practices. But when I do, React offers some great tools for render optimization like React.memo()
and React.useCallback
etc. However, there are cases where React is not able to deliver the satisfying results, and I like to discus one of them in this post: rapid changing styles.
Let's take a look at the problem with an example:
The problem
In this scenario, the problem is that we have to update the circle's style each time a mouse event is triggered. That means that we need to call setState multiple times a second. If these state changes affect only a small portion of your React render tree than you might be just fine. I mean it, look no further; don't optimize prematurely! But if you ever do run into these kind of performance issues... please continue reading 😉.
So basically we have two 'modes':
- Slow - the 'React' way
- Fast - the 'non-React' way and the thing we are going to build today
I'd like to note that this is a contrived example. This CodeSandbox is rendering 10,000 div's each render which aren't doing anything useful. So, in this case please take 'slow' with a grain of salt.
To be honest, I hate having to build something that isn't the React-way. And by the React-way I mean using React-state, creating uni-directional data-flows, which make things very predictable and easier to reason about. I wish React had some kind of mechanism to update rapid changes that are not mutating the dom tree, such as styles. A couple of month ago, I came across this tweet:
const now = useAnimation(isActive)
— Dan Abramov (@dan_abramov) June 3, 2019
Components that call it are opted into special traversal that happens every frame. That traversal calls render for these components but doesn’t allow tree changes beyond simple updates. Outside of animation loop “now” is NaN.
As far as I know, there has only been a short brainstorm, but it shows that the React team is aware of the 'problem'.
The non-React way?
What I mean by the non-React way, is using references to dom-nodes and manipulating these dom-nodes directly. React provides an escape hatch to do just that; it's called refs
. From the official React docs:
Refs provide a way to access DOM nodes or React elements created in the render method.
In our scenario we want to update a style directly. Let me show you an example of how that might work:
function Example() {
const myRef = React.useRef();
React.useEffect(() => {
// React assigned a reference to the dom-node
const domNode = myRef.current;
// manipulate the node directly
domNode.style.backgroundColor = "blue";
}, []);
return <div ref={myRef}>Hello World!</div>;
}
Before we start implementing a solution, I like to mention that there are already great tools available to tackle this problem. For instance:
- react-spring has a 'set' method to apply direct style updates
- framer-motion also has a 'set' method to apply direct style updates
Ok enough, let's start building!
Overview
What do we actually want to build? We want:
- a declarative way of setting styles directly (avoiding React-re-renders)
- a component that listens to these style updates
- something that handles the communication between us setting the style, and the component applying the style
Let's take a look at how that might look in code:
function Example() {
const [style, setStyle] = useSomeKindOfHook();
return (
<SomeKindOfComponent
style={style}
onMouseMove={({ clientX }) => setStyle({ left: clientX })}
/>
);
}
The hook
Alright, we haven't given this hook a name yet, haven't we?. Let's call it useDirectStyle
(for lack of a better name).
Now that we have the signature of the hook, we can write it's first lines of code:
function useDirectStyle() {
return {
style,
setStyle,
};
}
As described earlier, we need a way of directly communicating with the component that is interested in being directly styled. So we are going to expect a component to call and say: "Hey useDirectStyle-hook, I'm interested in your style updates. Here's my number (update function), let me know if you have some fresh styles".
This sounds a lot like a Observer pattern, doesn't it? Our hook is the subject here, communicating uni-directional to it's observers (the interested components).
function useDirectStyle() {
// keep track of the observers update functions
const subscriptions = React.useRef([]);
// allow the observer to subscribe
function subscribe(onUpdate) {
subscriptions.current.push(onUpdate);
// allow the observer to unsubscribe (ie. on unmount)
return function unsubscribe() {
subscriptions.current = subscriptions.current.filter(x => x !== onUpdate);
};
}
return {
style,
setStyle,
};
}
How are we going to get this subscribe
function to the interested component?...
function useDirectStyle() {
const subscriptions = React.useRef([]);
function subscribe(onUpdate) {
subscriptions.current.push(onUpdate);
return function unsubscribe() {
subscriptions.current = subscriptions.current.filter(x => x !== onUpdate);
};
}
return {
// We're using `useMemo` here because we want the style object to be
// referentially the same between renders
style: React.useMemo( () => ({ _subscribe: subscribe, }), [] ),
setStyle,
};
}
That's right, with help of the style
object.
But wait... isn't that misleading, we're passing a function in a style object? Yeah, it kinda does to be honest, but on the other hand, it keeps our api simple to understand. For the end user, we're just exposing a readable value (style
) and a setter (setStyle
). Besides, it's something they probably are already familiar with, like React.useState
. In other words, it's an implementation detail which the end-user doesn't need to be ware of, as long as we know what we're doing.
Finally, we need to implement setStyle
:
function useDirectStyle() {
const subscriptions = React.useRef([]);
function subscribe(onUpdate) {
subscriptions.current.push(onUpdate);
function unsubscribe() {
subscriptions.current = subscriptions.current.filter(x => x !== onUpdate);
}
return unsubscribe;
}
function setStyle(style) { subscriptions.current.forEach(updater => updater(style)); }
return {
// We're using `useMemo` here because we want the style object to be
// referentially the same between renders
style: React.useMemo(
() => ({
_subscribe: subscribe,
}),
[]
),
setStyle,
};
}
The component
Also we need to give this component a name. In this article we going to focus on div's only, so lets call it DirectStyledDiv
.
Basically it's just a div
, except that it communicates with our hook. Let's create the basis:
function DirectStyledDiv({ style, ...props }) {
// store a reference to the dom-element
// for future manipulations
const ref = React.useRef();
return <div ref={ref} {...props} />;
}
We expect to get a 'subscribe' method from our hook, hidden in the style
props. Let's establish communications:
function DirectStyledDiv({ style, ...props }) {
const ref = React.useRef();
React.useEffect(() => {
// we will write this in a minute...
function onUpdate() {}
// on mount, subscribe our `onUpdate` for future
// style updates
const unsubscribe = style._subscribe(onUpdate);
// on unmount, unsubscribe
return () => unsubscribe();
}, []);
return <div {...props} />;
}
We only need to react to the style updates coming from our hook:
function DirectStyledDiv({ style, ...props }) {
const ref = React.useRef();
React.useEffect(() => {
// we will write this in a minute...
function onUpdate(style) {
// loop over the given style's rules...
Object.keys(style).forEach(prop => { // ... assigning each rule to the dom-element's style ref.current.style[prop] = style[prop]; });
}
// on mount, subscribe our `onUpdate` for future
// style updates
const unsubscribe = style._subscribe(onUpdate);
// on unmount, unsubscribe
return () => unsubscribe();
}, []);
return <div {...props} />;
}
Production
DISCLAIMER: please don't copy-paste the above code examples into your production code; it's not production ready. The code examples above are over-simplified, and don't take into account the various edge cases.
I did, however, wrote a small library specially for this article. It's called direct-styled
, and if you like you can learn more about it on Github.
Conclusion
My goal was to give some insights on the issues you can run into when working with rapid style updates and ways how to solve them. React is very fast, and most of the time using simple state will suffice. Otherwise there are tools like react-spring
, framer-motion
and direct-styled
which can help you solve these performance issues. Or maybe you've gotten some inspiration from this article and you decide to write your own lib someday. I'd love to hear about it when you do!
Thanks for reading! I hope you liked it, and stay tuned for more articles! And please, don't be afraid to leave your feedback below.