Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Effector State Management Based on Dropdown

I'm using React Hooks and Effector to try and render data onto a card. The data will be driven off a React-Select dropdown (which represents different countries), and the idea is that the user will be able to add selections based on those countries, kind of like a TODO app.

However, I'm finding that whenever I go back to a previous dropdown selection, the data doesn't save. I'm using effector, hooks, react-select and react-jss for styling. The team gets passed as a prop from a React-Select component.

const PlayerComponent = ({ containerStyles, team }) => {

    const [items, setItems] = useState([]);

    const teamsStore = useStore(testTeams);
    const playersStore = useStore(testPlayers);

    useEffect(() => {

        const playersByTeam = playersStore.filter(
            (player) =>
                player.teamId ===
                teamsStore.find(function (t) {
                    return t.label === team;
                }).teamId
        );
    
        setItems(
            playersByTeam.map(function (player) {
                let players = { name: player.name };
                return players;
            })
        );
    }, [playersStore,team,teamsStore]);

    function onAddButtonClick() {
        setItems((prev) => {
            const newItems = [...prev];

            newItems.push({
                name: `Player ${newItems.length + 1}`
            });

            playersStore.push({
                name: `Player ${newItems.length + 1}`
            });

            return newItems;
        });
    }

    function renderAddButton() {
        return (
            <Row
                horizontal='center'
                vertical='center'
                onClick={onAddButtonClick}
            >
                Add Player
            </Row>
        );
    }

    return (
        <CardComponent
            containerStyles={containerStyles}
            title={team}
            items={[
                <Row horizontal='space-between' vertical='center'>
                    <span>
                        Create new player
                    </span>
                    {renderAddButton()}
                </Row>,
                ...items.map((item, index) => (
                    <Player index={index} item={item} />
                ))
            ]}
        />
    );
};

And this is my Player component, which represents a row each:

function Player({item = {} }) {
    return (
        <Row horizontal='space-between' vertical='center'>
            <Row>
                <span>{item.name}</span>
            </Row>
        </Row>
    );
}

Here's an example image. Basically, after selecting a country (say England) from a dropdown, I can add a player and it will render on the card- however, when I select a different country and go back the previous country, the added players disappear. Any ideas how to solve this? I'm cracking my head on the onAddButtonClick() function right now...

enter image description here

like image 783
clattenburg cake Avatar asked Nov 21 '20 15:11

clattenburg cake


1 Answers

I'm not an expert in Effector, but I think that like most state managers, it relies on state not being mutated, which is what you did when you pushed to playersStore. Using a reducer like I describe below is the correct solution to that.

Also, I'm a bit confused by what items is supposed to be, because as far as I can see, it's derived from the store and state and thus should not be state in its own right.

Finally, the use of useEffect does not make sense to me. Usually this is used to trigger an action upon rendering. What you really want to do here is much simpler... i.e. just take the current state and render it correctly.

I've simplified it a bit to remove stuff that isn't really relevant.

Basically it works like this. The store is a mapping of country names to player lists. When you need to make a change to this, you use this reducer which returns a brand new object with all the state of the old one, but where the country that is having a player added is also a new array, with the old contents, plus extra item added.

const onAddPlayer = (state, payload) => {
  const newState = { ...state };
  newState[payload["country"]] = [
    ...newState[payload["country"]],
    payload["player"]
  ];
  return newState;
};

// Create the store, registering the reducer for the addPlayer event.
const playersByCountry = createStore(initialStore).on(addPlayer, onAddPlayer);

In addition, there is a useState variable to keep track of the currently selected country.

Full code, but also on codesandbox.io

import React, { useState } from "react";
import { createStore, createEvent } from "effector";
import { useStore } from "effector-react";
import "./styles.css";
import Select from "react-select";

const initialStore = {
  France: [],
  Germany: ["Hans", "Kurt"],
  Italy: ["Francesca"]
};

const addPlayer = createEvent();

const onAddPlayer = (state, payload) => {
  const newState = { ...state };
  newState[payload["country"]] = [
    ...newState[payload["country"]],
    payload["player"]
  ];
  return newState;
};

const playersByCountry = createStore(initialStore).on(addPlayer, onAddPlayer);

export default () => {
  const pbc = useStore(playersByCountry);
  const options = Object.keys(pbc).map((item) => ({
    value: item,
    label: item
  }));
  const [country, setCountry] = useState(options[0]);
  const [playerInput, setPlayerInput] = useState("");
  const players = pbc[country["value"]];

  // When selector is changed, clear the input and set the
  // state `country` which is the current selection
  const handleCountryChange = (event) => {
    setCountry(event);
    setPlayerInput("");
  };

  // Controlled input state update
  const handlePlayerInputChange = (event) => {
    setPlayerInput(event.target.value);
  };

  // Pressing submit clears the input and adds the player
  // to the store
  const handlePlayerSubmit = (event) => {
    addPlayer({
      country: country["value"],
      player: playerInput
    });
    setPlayerInput("");
    event.preventDefault();
  };

  return (
    <>
      <Select
        defaultValue={country}
        options={options}
        onChange={handleCountryChange}
      />
      <div>
        <h1>Existing Players</h1>
        <ul>
          {players.map((player) => (
            <li>{player}</li>
          ))}
        </ul>
      </div>
      <form onSubmit={handlePlayerSubmit}>
        <label>
          Player:
          <input
            type="text"
            name="player"
            value={playerInput}
            onChange={handlePlayerInputChange}
          />
        </label>
        <input type="submit" value="Add" />
      </form>
    </>
  );
};
like image 114
dpwr Avatar answered Nov 15 '22 17:11

dpwr