I am trying to create a custom hook which will handle some merging of Aphrodite (https://github.com/Khan/aphrodite) styles for us, but I'm getting some unusual behaviour with the styles object being passed in.
Here's my hook: (the plan was to compose with useMemo which is why it's a hook at all)
import castArray from 'lodash/castArray';
import {StyleSheet} from 'aphrodite';
import merge from 'lodash/merge';
export const useStyles = (styles, props) => {
return {
styles: StyleSheet.create(
[...castArray(styles), ...castArray(props.styles)]
.reduce((agg, next) => (merge(agg, next))),
)
};
};
And basic usage:
function TestComponent(props) {
const {styles} = useStyles(baseStyles, props);
return <div className={css(styles.test)}>Test Text</div>
}
With the idea being that if a styles
prop is passed in, it will be merged with anything in baseStyles and give a final Aphrodite Stylesheet to be used for this instance of the component.
I've created a simple repro repository here: https://github.com/bslinger/hooks-question
Expectation: clicking between the two routes would change the colour of the text, based on whether the props have been passed in to override that style or not.
Actual: once the styles have been merged, even the route without the additional props being passed in shows it with the overridden colour.
Note: I realised that after removing useMemo this wasn't even technically a hook, and downgrading React to 16.7 resulted in the same behaviour, so I guess this is just a Javascript or React question after all?
The key here is understanding the behavior of Array.reduce in detail. reduce
takes in two arguments. The first argument is the callback which you specified. The second argument is optional and is the initial value for the accumulator (first argument of the callback). Here is the description of that argument:
Value to use as the first argument to the first call of the callback. If no initial value is supplied, the first element in the array will be used. Calling reduce() on an empty array without an initial value is an error.
To understand the effect of this more easily, it will help to simplify your syntax.
So long as styles
and props.styles
are not arrays (which they aren't in your sample), the following:
[...castArray(styles), ...castArray(props.styles)]
is equivalent to:
[styles, props.styles]
So in the absence of an initialValue to the reduce
function, the accumulator is going to be the first element in your array: styles
. So once you execute the "withProps" scenario, you have mutated the object in styles.js
and nothing will change it back to the original green. If styles
was an array (using the original code), then this side-effect would occur to the first style object in that array.
To fix this, you just need to specify an initial value for the accumulator:
export const useStyles = (styles, props) => {
return {
styles: StyleSheet.create(
[...castArray(styles), ...castArray(props.styles)].reduce(
(agg, next) => merge(agg, next),
{} // Here's an empty object as the accumulator initial value
)
)
};
};