Does using the combination of useReducer with too many useEffects an anti-pattern?

208 Views Asked by At

I have created a Select Dropdown component with its state abstracted in the reducer. I am just updating this collective state using reducer actions and handling the side effects via useEffect but there are too many useEffects now and made me wonder is this the right approach at all? Also, when I am using reducer actions to update this collective state, are these changes qualified to be called side-effects at all? Sharing two code blocks below:

  1. List.jsx with logic of the component
  2. List.reducer.js with reducer logic in it.

List.jsx

import React, { useRef, useCallback, useReducer, useEffect } from "react"
import PropTypes from "prop-types"
import "./list.style.sass"
import ListItem from "../../pure-components/list-item/list-item.jsx"
import TextField from "../../pure-components/text-field/text-field.jsx"
import PrimaryButton from "../../pure-components/primary-button/primary-button.jsx"
import SecondaryButton from "../../pure-components/secondary-button/secondary-button.jsx"
import { useFilterListSearch, useClickOutside, useKeyPress } from "../../hooks"
import { nanoid } from "nanoid"
import { reducer, initialState, ACTION } from "./list.reducer.js"

const List = ({
    list,
    searchable,
    button,
    defaultItem,
    searchWhichKeys,
    onSelect,
}) => {
    /**
     * State of this component
     */
    const [state, dispatch] = useReducer(reducer, initialState)
    const filteredList = useFilterListSearch(
        list, // Default list
        state.searchQuery,
        searchWhichKeys
    )

    const listRef = useRef(null)
    const escPress = useKeyPress("Escape")

    const closeList = useCallback(
        (event) => {
            if (event) return
            dispatch({ type: ACTION.HIDE_LIST })
        },
        [state.listVisibility]
    )

    useClickOutside(listRef, closeList)

    const onItemClicked = (item) => {
        dispatch({ type: ACTION.HIDE_LIST })
        dispatch({ type: ACTION.UPDATE_USER_SELECTION, payload: item })
    }

    const callbackOnButtonClick = useCallback(
        (buttonPressedState, fn) => {
            const buttonState = buttonPressedState
                ? ACTION.SHOW_LIST
                : ACTION.HIDE_LIST

            console.count([
                buttonState,
                "because button-pressed is",
                buttonPressedState,
                fn,
                state.buttonUID,
            ])
            dispatch({ type: buttonState })
        },
        [state.buttonUID]
    )

    const renderSelectButton = useCallback(() => {
        const buttonProps = {
            displayTooltip: true,
            label: state.userSelection
                ? state.userSelection.option
                : button?.label,
            icon: button?.icon,
            callback: callbackOnButtonClick,
            keepPressedAfterClick: true,
        }

        return button.type === "primary" ? (
            <PrimaryButton key={state.buttonUID} {...buttonProps} />
        ) : (
            <SecondaryButton key={state.buttonUID} {...buttonProps} />
        )
    }, [state.buttonUID])

    const resetButonState = () => {
        /**
         * Resets the button's state whenever the list's visibility
         * is transitoned from shown to hidden. We are not resetting
         * the UID in case of state transition from hidden to
         * visible as it's resetting button's state on each click
         *
         * Therefore, if list is visible we will not change the UID only when it
         * is made to hide, we will reset the button state as well
         */
        if (state.listVisibility) return
        dispatch({ type: ACTION.UPDATE_BUTTON_UID, payload: nanoid() })
    }

    const resetToDefaultList = () => {
        /**
         * Whenever, we open the list we want it to be the default list
         * not filtered one from the previously applied filter (if any)
         * Therefore, we will reset it to default whenever it is closed.
         * Which is equivalent of setting the user query to null
         */
        if (state.listVisibility) return
        dispatch({ type: ACTION.RESET_SEARCH_QUERY })
    }

    useEffect(() => {
        /**
         * Whenever the default item provided changes, we update
         * the userSelectin state with default selection
         */
        dispatch({ type: ACTION.UPDATE_USER_SELECTION, payload: defaultItem })
    }, [defaultItem])

    useEffect(() => {
        /**
         * Initially we fill the dropdown list with the items in list prop and
         * again do the same whenever this provided list changes
         */
        dispatch({ type: ACTION.UPDATE_LIST, payload: list })
    }, [list])

    useEffect(() => {
        /**
         * We update the ref list state whenever the reference to the list of
         * this component changes
         */
        dispatch({ type: ACTION.UPDATE_LIST_REF, payload: listRef })
    }, [listRef])

    useEffect(() => {
        /**
         * Reset the pressed state fof the button
         */
        resetButonState()
        /**
         * Resets to the default list
         */
        resetToDefaultList()
    }, [state.listVisibility])

    useEffect(() => {
        if (state.userSelection === "") return
        /**
         * Whenever user selects an item we call onSelect callback and
         * pass this user selected item as an argument of it
         */
        onSelect(state.userSelection)
    }, [state.userSelection])

    useEffect(() => {
        /**
         * Whenever custom hook filtered list changes we'll sync
         * the resultant list with component's state
         */
        dispatch({ type: ACTION.UPDATE_LIST, payload: filteredList })
    }, [filteredList])

    useEffect(() => {
        /**
         * If Esc is pressed hide the list else do nothing
         */
        if (!escPress) return
        dispatch({ type: ACTION.HIDE_LIST })
    }, [escPress])

    return (
        <div className="List" ref={listRef}>
            <div className="list-select">{renderSelectButton()}</div>

            {state.listVisibility ? (
                <div className="list-container">
                    {searchable ? (
                        <div className="list-header">
                            <TextField
                                name="search"
                                placeholder="Search..."
                                onChangeCallback={(event) => {
                                    dispatch({
                                        type: ACTION.UPDATE_SEARCH_QUERY,
                                        payload: event?.target?.value,
                                    })
                                }}
                            />
                        </div>
                    ) : null}
                    <div className="list-body">
                        {state.list.length
                            ? state.list.map((item) => {
                                return (
                                    <ListItem
                                        key={item.id}
                                        item={item}
                                        isActive={
                                            state.userSelection?.id ===
                                              item?.id
                                        }
                                        onClickCallback={onItemClicked}
                                    />
                                )
                            })
                            : null}
                    </div>
                </div>
            ) : null}
        </div>
    )
}

List.propTypes = {
    list: PropTypes.array.isRequired,
    searchable: PropTypes.bool,
    button: PropTypes.object.isRequired,
    defaultItem: PropTypes.object,
    searchWhichKeys: PropTypes.array,
    onSelect: PropTypes.func,
}

List.defaultProps = {
    list: [
        {
            id: nanoid(),
            option: "Anything",
        },
    ],
    searchable: false,
    button: {
        type: "secondary",
        icon: null,
        label: "Button",
    },
    defaultItem: null,
    searchWhichKeys: ["option"],
    onSelect: () => {},
}

export default List

List.reducer.js

/**
 * Initial state
 */
export const initialState = {
    listVisibility: false,
    userSelection: "",
    searchQuery: "",
    buttonUID: "",
    list: [],
    refs: {
        list: null,
    },
    error: {
        status: false,
        message: "",
    },
}
/**
 * Reducer function
 */
export const reducer = (state, action) => {
    switch (action.type) {
    case ACTION.SHOW_LIST:
        return { ...state, listVisibility: true }

    case ACTION.HIDE_LIST:
        return { ...state, listVisibility: false }

    case ACTION.UPDATE_USER_SELECTION:
        return { ...state, userSelection: action.payload }

    case ACTION.UPDATE_SEARCH_QUERY:
        return { ...state, searchQuery: action.payload }

    case ACTION.RESET_SEARCH_QUERY:
        return { ...state, searchQuery: "" }

    case ACTION.UPDATE_LIST:
        return { ...state, list: action.payload }

    case ACTION.UPDATE_BUTTON_UID:
        return { ...state, buttonUID: action.payload }

    case ACTION.UPDATE_LIST_REF:
        return {
            ...state,
            refs: {
                ...state.refs,
                list: action.payload,
            },
        }

    case ACTION.THROW_ERROR:
        return {
            ...state,
            error: {
                ...state.error,
                status: true,
                message: action.payload,
            },
        }

    case ACTION.CLEAR_ERROR:
        return {
            ...state,
            error: { ...state.error, status: false, message: "" },
        }

    default:
        throw new Error("Problematic Action: ", action)
    }
}
/**
 * Reducer Actions
 */
export const ACTION = {
    SHOW_LIST: "show-list",
    HIDE_LIST: "hide-list",
    UPDATE_USER_SELECTION: "update-user-selection",
    UPDATE_SEARCH_QUERY: "update-search-query",
    RESET_SEARCH_QUERY: "reset-search-query",
    UPDATE_LIST: "update-list",
    UPDATE_BUTTON_UID: "update-button-uid",
    UPDATE_LIST_REF: "update-list-ref",
    THROW_ERROR: "throw-error",
    CLEAR_ERROR: "clear-error",
}


0

There are 0 best solutions below