1

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.

user42195
  • 419
  • 4
  • 16
  • Can you clarify what any issue is, and provide a [minimal, complete, and reproducible code example](https://stackoverflow.com/help/minimal-reproducible-example)? Your codesandbox also appears to have a render looping issue. – Drew Reese Mar 03 '22 at 02:44

2 Answers2

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 />
    </>
  );
}
Fody
  • 23,754
  • 3
  • 20
  • 37
1

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>
  );
}
Jacob K
  • 1,096
  • 4
  • 10
  • Thank you so much. And I tried to set the label as you adviced. But when I fire onAction, it's not working like this. Do you have any idea?https://codesandbox.io/s/recursing-engelbart-i9bc6g?file=/App.js – user42195 Mar 03 '22 at 08:43
  • I see you updated the state to include labels as I suggested, but then the problem is that the toggle function didn't change to accommodate the new state shape. Then, you also want to update the map within the JSX to destructure the state of each toggle (since it is now an array of objects instead of an array of booleans). Here is a [forked CodeSandbox](https://codesandbox.io/s/stupefied-stitch-xp3yh5?file=/App.js) from yours with those updates working properly. – Jacob K Mar 03 '22 at 17:58
  • Thank you so much. I had no idea to implement this part ` { ...prev[i], isActive: !prev[i].isActive },`. I will understand and acquire the knowledge. I really appreciate it. – user42195 Mar 04 '22 at 00:49
  • [This answer](https://stackoverflow.com/a/49491435/14077491) may provide some insight into this. With that line, we're using the [spread operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) and overwriting a particular property, in this case, "isActive". – Jacob K Mar 04 '22 at 01:30
  • Oh wow. It makes clearer how to use object's spread operator. Thank you for your advice. It helps me a lot. https://stackblitz.com/edit/node-e3mbfv?file=index.js – user42195 Mar 04 '22 at 01:47