skip to Main Content

I use Shopify Polaris’s setting toggle.https://polaris.shopify.com/components/actions/setting-toggle#navigation

And I want to implement not only one but multi setting toggles.But I don’t want to always duplicate same handleToggle() and values(contentStatus, textStatus) like below the sandbox A,B,C…

import React, { useCallback, useState } from "react";
import { SettingToggle, TextStyle } from "@shopify/polaris";

export default function SettingToggleExample() {
  const [activeA, setActiveA] = useState(false);
  const [activeB, setActiveB] = useState(false);

  const handleToggleA = useCallback(() => setActiveA((active) => !active), []);
  const handleToggleB = useCallback(() => setActiveB((active) => !active), []);

  const contentStatusA = activeA ? "Deactivate" : "Activate";
  const contentStatusB = activeB ? "Deactivate" : "Activate";
  const textStatusA = activeA ? "activated" : "deactivated";
  const textStatusB = activeB ? "activated" : "deactivated";

  const useHandleToggle = (active, setActive) => {
    const handleToggle = useCallback(() => setActive((active) => !active), []);

    const contentStatus = active ? "Disconnect" : "Connect";
    const textStatus = active ? "connected" : "disconnected";
    handleToggle();
    return [contentStatus, textStatus];
  };

  useHandleToggle(activeA, setActiveA);

  return (
    <>
      <SettingToggle
        action={{
          content: contentStatusA,
          onAction: handleToggleA
        }}
        enabled={activeA}
      >
        This setting is <TextStyle variation="strong">{textStatusA}</TextStyle>.
      </SettingToggle>
      <SettingToggle
        action={{
          content: contentStatusB,
          onAction: handleToggleB
        }}
        enabled={activeB}
      >
        This setting is <TextStyle variation="strong">{textStatusB}</TextStyle>.
      </SettingToggle>
    </>
  );
}

https://codesandbox.io/s/vigorous-pine-k0dpib?file=/App.js

So I thought I can use a custom hook. But it’s not working. So it would be helpful if you give me some advice.

2

Answers


  1. My first attempt to refactor would use a parameter on the common handler

    const handleToggle = useCallback((which) => {
      which === 'A' ? setActiveA((activeA) => !activeA) 
       : setActiveB((activeB) => !activeB)
    },[])
    
    ...
    
    <SettingToggle
      action={{
        content: contentStatusA,
        onAction: () => handleToggle('A')
      }}
      enabled={activeA}
    >
    

    It functions, but feels a bit naïve. For something more React-ish, a reducer might be the way to go.


    With a reducer

    This seems cleaner, and is definitely more extensible if you need more toggles.

    function reducer(state, action) {
      switch (action.type) {
        case "toggleA":
          const newValueA = !state.activeA;
          return {
            ...state,
            activeA: newValueA,
            contentStatusA: newValueA ? "Deactivate" : "Activate",
            textStatusA: newValueA ? "activated" : "deactivated"
          };
        case "toggleB":
          const newValueB = !state.activeB;
          return {
            ...state,
            activeB: newValueB,
            contentStatusB: newValueB ? "Deactivate" : "Activate",
            textStatusB: newValueB ? "activated" : "deactivated"
          };
        default:
          throw new Error();
      }
    }
    
    const initialState = {
      activeA: false,
      activeB: false,
      contentStatusA: "Activate",
      contentStatusB: "Activate",
      textStatusA: "deactivated",
      textStatusB: "deactivated"
    };
    
    export default function SettingToggleExample() {
      const [state, dispatch] = useReducer(reducer, initialState)
    
      return (
        <>
          <SettingToggle
            action={{
              content: state.contentStatusA,
              onAction: () => dispatch({type: 'toggleA'})
            }}
            enabled={state.activeA}
          >
            This setting is <TextStyle variation="strong">{state.textStatusA}</TextStyle>.
          </SettingToggle>
          <SettingToggle
            action={{
              content: state.contentStatusB,
              onAction: () => dispatch({type: 'toggleA'})
            }}
            enabled={state.activeB}
          >
            This setting is <TextStyle variation="strong">{state.textStatusB}</TextStyle>.
          </SettingToggle>
        </>
      );
    }
    

    With a wrapper component

    A child component can eliminate the ‘A’ and ‘B’ suffixes

    function reducer(state, action) {
      switch (action.type) {
        case "toggle":
          const newValue = !state.active;
          return {
            ...state,
            active: newValue,
            contentStatus: newValue ? "Deactivate" : "Activate",
            textStatus: newValue ? "activated" : "deactivated"
          };
        default:
          throw new Error();
      }
    }
    
    const initialState = {
      active: false,
      contentStatus: "Activate",
      textStatus: "deactivated",
    };
    
    const ToggleWrapper = () => {
      const [state, dispatch] = useReducer(reducer, initialState);
      return (
        <SettingToggle
          action={{
            content: state.contentStatus,
            onAction: () => dispatch({ type: "toggle" })
          }}
          enabled={state.active}
        >
          This setting is <TextStyle variation="strong">{state.textStatus}</TextStyle>.
        </SettingToggle>
      )
    }
    
    export default function SettingToggleExample() {
    
      return (
        <>
          <ToggleWrapper />
          <ToggleWrapper />
        </>
      );
    }
    
    Login or Signup to reply.
  2. Using simple Booleans for each toggle

    If you combine your active state objects into a single array, then you can update as many settings as you would like dynamically. Here’s an example of what that might look like:

    import React, { useCallback, useState } from "react";
    import { SettingToggle, TextStyle } from "@shopify/polaris";
    
    export default function SettingToggleExample() {
      // define stateful array of size equal to number of toggles
      const [active, setActive] = useState(Array(2).fill(false));
    
      const handleToggle = useCallback((i) => {
        // toggle the boolean at index, i
        setActive(prev => [...prev.slice(0,i), !prev[i], ...prev.slice(i+1)])
      }, []);
    
      return (
        <>
          {activeStatuses.map((isActive, index) =>
            <SettingToggle
              action={{
                content: isActive ? "Deactivate" : "Activate",
                onAction: () => handleToggle(index)
              }}
              enabled={isActive}
            >
              This setting is <TextStyle variation="strong">{isActive ? "activated" : "deactivated"}</TextStyle>.
            </SettingToggle>
          }
        </>
      );
    }
    

    Of course, you will likely want to add a label to each of these going forward, so it may be better to define a defaultState object outside the function scope and replace the Array(2).fill(false) with it. Then you can have a string label property for each toggle in addition to a boolean active property which can be added next to each toggle in the .map(...).

    With labels added for each toggle

    Per your follow up, here is the implementation also found in the CodeSandbox for a state with labels for each toggle (including here on the answer to protect against link decay):

    import React, { useCallback, useState } from "react";
    import { SettingToggle, TextStyle } from "@shopify/polaris";
    
    const defaultState = [
      {
        isActive: false,
        label: "A"
      },
      {
        isActive: false,
        label: "B"
      },
      {
        isActive: false,
        label: "C"
      }
    ];
    
    export default function SettingToggleExample() {
      const [active, setActive] = useState(defaultState);
    
      const handleToggle = useCallback((i) => {
        // toggle the boolean at index, i
        setActive((prev) => [
          ...prev.slice(0, i),
          { ...prev[i], isActive: !prev[i].isActive },
          ...prev.slice(i + 1)
        ]);
      }, []);
    
      return (
        <div style={{ height: "100vh" }}>
          {active?.map(({ isActive, label }, index) => (
            <SettingToggle
              action={{
                content: isActive ? "Deactivate" : "Activate",
                onAction: () => handleToggle(index)
              }}
              enabled={isActive}
              key={index}
            >
              This {label} is 
              <TextStyle variation="strong">
                {isActive ? "activated" : "deactivated"}
              </TextStyle>
              .
            </SettingToggle>
          ))}
        </div>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search