动态设置React Media Queries

时间:2019-11-16 11:46:56

标签: javascript reactjs responsive-design media-queries react-hooks

React组件(Viewer)包含元素(Element)的动态列表。元素的大小取决于当前的屏幕分辨率。

以下将屏幕分辨率配置动态传递给Viewer组件:

<Viewer
    elements={[{
        uid: '1',
        title: 'project 1'
    }, {
        uid: '2',
        title: 'project 2'
    }, {
        uid: '3',
        title: 'project 3'
    }, {
        uid: '4',
        title: 'project 4'
    }]}
    configurations={[
        {
            name: 'large screen',
            width: 1200,
            gridRowSize: 4
        },
        {
            name: 'tablet',
            width: 800,
            gridRowSize: 2
        },
        {
            name: 'phone',
            width: 480,
            gridRowSize: 1
        }
    ]}/>

configurations属性是动态的,Viewer组件由于来自REST API,因此无法对其进行任何假设。配置和元素彼此不相关。

我正在使用react-responsive来达到以下目的:

import {useMediaQuery} from 'react-responsive';

export default function Viewer(props) {
    const configurations = [];

    if (props.configurations) {
        for (const [index, configuration] of props.configurations.sort((a, b) => (a.width - b.width)).entries()) {
            configurations.push({
                // props.configurations does not change between renders, so it's safe to disable the lint check
                // eslint-disable-next-line react-hooks/rules-of-hooks
                isActive: useMediaQuery({
                    ...index > 0 && {minWidth: props.configurations[index - 1].width},
                    ...index < (props.configurations.length - 1) && {maxWidth: configuration.width}
                }),
                size: 100 / configuration.gridRowSize
            });
        }
    }

    const activeConfiguration = configurations.find(config => config.isActive);
    const size = activeConfiguration ? activeConfiguration.size : 25;

    return (
        <div className={'viewer'}>
            {
                props.elements && props.elements.map((element) => (
                    <Element
                        key={element.uid}
                        id={element.uid}
                        title={element.title}
                        size={`${size}%`}/>
                ))
            }
        </div>
    );
}

问题在于上面的实现违反了此处所述的Hooks规则之一:https://reactjs.org/docs/hooks-rules.html#eslint-plugin(React依赖于调用Hooks的顺序,并且循环理论上可以破坏该顺序)。

在不禁用棉绒检查的情况下,运行eslint时出现此错误:

React Hook "useMediaQuery" may be executed more than once. Possibly because it is called in a loop. 
React Hooks must be called in the exact same order in every component render  react-hooks/rules-of-hooks

由于props.configurations在渲染之间不会更改,因此我目前正在禁用棉绒错误检查,但是我想知道是否有更好的方法使用react-responsive配置动态媒体查询?

1 个答案:

答案 0 :(得分:0)

我还没有使用react-responsive,但我想我理解这个问题。您有一个道具(配置)的动态列表,每个道具都需要传递到一个挂钩,该挂钩将定义要传递给组件(元素)的某个属性(大小)。问题在于所有这些逻辑都不应在单个父组件(查看器)中处理。

如果可以直接使用useMediaQuery的功能,这将使其更加整洁。如果没有,您可以为每个配置元素创建一个具有自己的挂钩的组件。下面的代码假定总会有一个匹配的查询。让我知道是否是这种情况。

export const Viewer = props => {
    const { elements, configurations } = props;

    const [active_size, setActiveSize] = useState(25);

    return (
        <div className={"viewer"}>
            {configurations.map((configuration, index) => (
                <Configuration
                    key={configuration.name}
                    {...configuration}
                    index={index}
                    setActiveSize={setActiveSize}
                    configurations={configurations}
                />
            ))}
            {elements.map(({ uid, title }) => (
                <Element key={uid} id={uid} title={title} size={`${active_size}%`} />
            ))}
        </div>
    );
};

export const Configuration = props => {
    const { width, configurations, index, setActiveSize, gridRowSize } = props;

    const is_active = useMediaQuery({
        ...(index > 0 && { minWidth: configurations[index - 1].width }),
        ...(index < configurations.length - 1 && {
            maxWidth: width
        })
    });

    useEffect(() => {
        if (is_active) {
            setActiveSize(100 / gridRowSize);
        }
    }, [is_active]);

    return null;
};