简体   繁体   中英

Passing imported style object into a custom hook does not work as expected

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
      )
    )
  };
};

编辑 yqjjmmp21x

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM