2
\$\begingroup\$

I have a react native native app and I am fetching the state of the toggle buttons from an API and then putting them into redux state. I do not like the way I have implemented it cause it seems very messy and was wondering if anyone would have any suggestions on how to refactor the code.

Below is screen that calls my redux state for each toggle switch.

const SiteSettingsDashboard = (props) => {
  const dispatch = useDispatch();

  const alarmNotificationsToggle = useSelector(
    (state) => state.registration.alarmNotifications
  );
  const armingNotificationToggle = useSelector(
    (state) => state.registration.armingNotifications
  );

  const faultsNotificationToggle = useSelector(
    (state) => state.registration.faultsNotifications
  );

  const engineerNotificationToggle = useSelector(
    (state) => state.registration.engineerNotifications
  );

  const techZoneNotificationToggle = useSelector(
    (state) => state.registration.techZoneNotifications
  );

  const eventTimerNotificationToggle = useSelector(
    (state) => state.registration.eventTimerNotifications
  );

  const noCommsNotificationToggle = useSelector(
    (state) => state.registration.noCommsNotifications
  );

  const ToggleSwitchInput = (value) => {
    dispatch(registrationActions.toggleNotificationInput(value));
  };

  return (
    <View>
        <View style={{ flexDirection: "row" }}>
          <Text>{IMLocalized("Arming")}</Text>
          <Switch
            style={styles.right}
            trackColor={{ false: "#FF0000", true: "#008000" }}
            thumbColor={armingNotificationToggle ? "#008000" : "#FF0000"}
            ios_backgroundColor="#3e3e3e"
            onValueChange={() => ToggleSwitchInput("arming")}
            value={armingNotificationToggle}
          />
        </View>
        <Card.Divider />
        <View style={{ flexDirection: "row" }}>
          <Text>{IMLocalized("Faults")}</Text>
          <Switch
            style={styles.right}
            trackColor={{ false: "#FF0000", true: "#008000" }}
            thumbColor={faultsNotificationToggle ? "#008000" : "#FF0000"}
            ios_backgroundColor="#3e3e3e"
            onValueChange={() => ToggleSwitchInput("faults")}
            value={faultsNotificationToggle}
          />
        </View>
        <Card.Divider />
        <View style={{ flexDirection: "row" }}>
          <Text>{IMLocalized("Engineer")}</Text>
          <Switch
            style={styles.right}
            trackColor={{ false: "#FF0000", true: "#008000" }}
            thumbColor={engineerNotificationToggle ? "#008000" : "#FF0000"}
            ios_backgroundColor="#3e3e3e"
            onValueChange={() => ToggleSwitchInput("engineer")}
            value={engineerNotificationToggle}
          />
        </View>
        <Card.Divider />
        <View style={{ flexDirection: "row" }}>
          <Text>{IMLocalized("TechZone")}</Text>
          <Switch
            style={styles.right}
            trackColor={{ false: "#FF0000", true: "#008000" }}
            thumbColor={techZoneNotificationToggle ? "#008000" : "#FF0000"}
            ios_backgroundColor="#3e3e3e"
            onValueChange={() => ToggleSwitchInput("techZone")}
            value={techZoneNotificationToggle}
          />
        </View>
        <Card.Divider />
        <View style={{ flexDirection: "row" }}>
          <Text>{IMLocalized("EventTimers")}</Text>
          <Switch
            style={styles.right}
            trackColor={{ false: "#FF0000", true: "#008000" }}
            thumbColor={eventTimerNotificationToggle ? "#008000" : "#FF0000"}
            ios_backgroundColor="#3e3e3e"
            onValueChange={() => ToggleSwitchInput("eventTimer")}
            value={eventTimerNotificationToggle}
          />
        </View>
        <Card.Divider />
        <View style={{ flexDirection: "row" }}>
          <Text>{IMLocalized("NoComms")}</Text>
          <Switch
            style={styles.right}
            trackColor={{ false: "#FF0000", true: "#008000" }}
            thumbColor={noCommsNotificationToggle ? "#008000" : "#FF0000"}
            ios_backgroundColor="#3e3e3e"
            onValueChange={() => ToggleSwitchInput("noComms")}
            value={noCommsNotificationToggle}
          />
        </View>
        <Card.Divider />
      </Card>
    </View>
  );
};

const styles = StyleSheet.create({
  errorText: {
    color: "red",
  },
  right: {
    flex: 1,
  },
});

export default SiteSettingsDashboard; 

I have the state of each of the buttons declared in redux.

export const types = Object.freeze({
  ToggleAlarmNotification: "TOGGLE_ALARM_NOTIFICATION",
  ToggleArmingNotification: "TOGGLE_ARMING_NOTIFICATION",
  ToggleFaultsNotification: "TOGGLE_FAULTS_NOTIFICATION",
  ToggleEngineerNotification: "TOGGLE_Engineer_NOTIFICATION",
  ToggleTechZoneNotification: "TOGGLE_TECH_ZONE_NOTIFICATION",
  ToggleEventTimerNotification: "TOGGLE_EVENT_TIMER_NOTIFICATION",
  ToggleNoCommsNotification: "TOGGLE_NO_COMMS_NOTIFICATION",
});


const initialState = {
  isRequestingLogoImage: false,
  alarmNotifications: false,
  armingNotifications: false,
  faultsNotifications: false,
  engineerNotifications: false,
  techZoneNotifications: false,
  eventTimerNotifications: false,
  noCommsNotifications: false,
};

And here is my reducer file. I do this for all of the toggle buttons.

case types.ToggleNoCommsNotification:
      return {
        ...state,
        noCommsNotifications: !state.noCommsNotifications,
      };
\$\endgroup\$
2
  • 1
    \$\begingroup\$ I can look at this more closely later, but the way that you would avoid all of this special-casing is to have the the notification type ('alarm', 'faults', etc.) be a variable. It seems that you've done this with ToggleSwitchInput, but not with your action types or your selectors. You don't need to have a separate action type constant for every toggle. You can use the payload to pass the toggle name. \$\endgroup\$ – Linda Paiste Oct 22 '20 at 9:53
  • 1
    \$\begingroup\$ Plus all of your Card components are essentially the same! I can get this code down to half the size if not less. But I need to go to bed first. \$\endgroup\$ – Linda Paiste Oct 22 '20 at 9:56
5
+150
\$\begingroup\$

The general idea here is that we have seven different toggles and we don't want to write the same code seven times in every place. We want to define functions that act on a general notification and pass in the name of the notification as an argument.

Actions

We can define one action type "TOGGLE_NOTIFICATION" and use the action.payload property to pass through the name of the notification that we are toggling. An action creator will create the right action from just the name.

export const TOGGLE_NOTIFICATION = "TOGGLE_NOTIFICATION";

export const toggleNotification = (name) => ({
  type: TOGGLE_NOTIFICATION,
  payload: {
    name
  }
});

State

Right now the toggles are top-level properties alongside isRequestingLogoImage, but I think it's better to give them their own branch. One advantage of separating them is is that we could use Object.keys() in a selector to get all of the canonical notification names.

const initialNotifications = {
  alarm: false,
  arming: false,
  faults: false,
  engineer: false,
  techZone: false,
  eventTimer: false,
  noComms: false
}

We don't actually need a combined initial state, but it would be this:

const initialState = {
  isRequestingLogoImage: false,
  notifications: initialNotifications,
};

Reducer

We only have to handle one action now instead of seven! The action.payload.name property is the key which we are updating and we set it to the opposite of the existing value.

const notifications = (state = initialNotifications, action) => {
  switch (action.type) {
    case TOGGLE_NOTIFICATION: {
      return {
        ...state,
        [action.payload.name]: !state[action.payload.name]
      };
    }
    default:
      return state;
  }
};

That reducer updates the notifications property only. We combine this branch with the other reducers in your app using the redux combineReducers function.

// dummy placeholder
const isRequestingLogoImage = (state = false, action ) => state;

const rootReducer = combineReducers({
  isRequestingLogoImage,
  notifications
});

export default rootReducer;

Selectors

We can get the current value for each toggle by defining an isEnabled selector as a double arrow function that takes the notification name as the first argument and the state as the second. The double function means that instead of calling useSelector(state => isEnabled(state, name)) we can just call useSelector(isEnabled(name)).

As mentioned before, we can also select all of the notification types in order to loop through them.

export const isEnabled = (name) => (state) => state.notifications[name];

export const getNotificationTypes = (state) => Object.keys(state.notifications);

Components

We want to create a generalized ToggleSwitch component which we can use for all notification types rather than writing the same style properties seven different times. This component takes the name/key of the notification as a prop and also accepts an optional label prop so that you can pass in the title-cased version of the name.

We use redux hooks to get the current on/off value and to dispatch the onValueChange callback.

const ToggleSwitch = ({name, label}: ToggleSwitchProps) => {
  const value = useSelector(isEnabled(name));
  const dispatch = useDispatch();

  return (
    <View style={{ flexDirection: "row" }}>
      <Text>{IMLocalized(label || name)}</Text>
      <Switch
        style={styles.right}
        trackColor={{ false: "#FF0000", true: "#008000" }}
        thumbColor={value ? "#008000" : "#FF0000"}
        ios_backgroundColor="#3e3e3e"
        onValueChange={() => dispatch(toggleNotification(name))}
        value={value}
      />
    </View>
  )
}

Our SiteSettingsDashboard is now just a bunch of ToggleSwitch JSX declarations. You can manually write them out if you want to control the order and the label text. This is still much simpler than before because we only have one component with two properties instead of a whole View/Text/Switch block.

But as your current titles are just the notification names in PascalCase, we can loop through and use lodash string functions to create the label.

const SiteSettingsDashboard = () => {
  const notifications = useSelector(getNotificationTypes);

  return (
    <View>
      {notifications.map((name) => (
        <ToggleSwitch
          key={name}
          name={name}
          label={upperFirst(camelCase(name))}
        />
      ))}
    </View>
  );
};

Complete CodeSandbox Link (with type annotations because I think in typescript)

\$\endgroup\$
1
  • \$\begingroup\$ Thanks Linda. I tried writing something similar myself but I had some difficulty doing it. \$\endgroup\$ – Great Khan 2016 Oct 23 '20 at 8:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.