Neuroneer
Neuroneer

Reputation: 95

How do I change the visibility of certain components in React as I am mapping through them?

Without over-complicating the premise, I'm attempting to build a word-game in React that maps through components and alters their visibility based off of when they are being rendered in a loop. This is my first attempt at building a game in React, so I apologize in advance for my general lack of understanding. I'm capable of building the same logic within vanilla JavaScript, but within React, I'm still quite lost when it comes to targeting components and altering their state based off of the data that they possess.

Within my game, I am attempting to render an 8x9 column-oriented grid, with each column being 9 values high and each row being 8 values across. However, upon initially loading the game, I only want the first three rows of components to be visible. The goal is for more components to render over time based off of a set-timeout function, but that is a problem for later. The way I am used to thinking about this problem is by mapping over an array of values, doing X to values that are not null, and doing Y to values that are null. Hence, I have written my components as such:

const Letter = ({ letter }) => {
// This component takes in a random letter and sends the letter to a form input element when clicked

const [clicked, setClicked] = useState(false);
const [currLetter, setCurrLetter] = useState('');

const { inputVal, setInputVal} = useInputContext();

useEffect(() => {
    const currValues = [];
    
    if (clicked) currValues.push(`${currLetter}`)
    setInputVal(inputVal.concat(currValues))
}, [clicked])

return <div className='letters' disabled={clicked} onClick={() => {
    setClicked((clicked) => !clicked);
    setCurrLetter(`${letter}`)
}}>{letter}</div>
};
export default Letter;

...next component

const Column = () => {
const [randomCol, setRandomCol] = useState([]);

const vowels = ['A', 'E', 'I', 'O', 'U', 'Y'];
const consonants = ['B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'R', 'S', 'T', 'V', 'W']
const goldConsonants = ['X', 'Z', 'Q'];

const randomGen = Math.floor(Math.random() * 100);
const letterGenerator = () => {
// Generates a random letter that letter gets passed into my letter component
    const randomNum = randomGen;

    if (randomNum >= 40) return consonants[Math.floor((Math.random()*consonants.length))];
    if (randomNum >= 5 && randomNum < 40) return vowels[Math.floor((Math.random()*vowels.length))];

    return goldConsonants[Math.floor((Math.random()*goldConsonants.length))];
};

useEffect(() => {
    const col = [];

    // Renders the letter components
    for (let j = 0; j < 9; j++) {
        // These last three values in my columns should be visible (last three rows)
        if (j >= 6) col.push(<Letter letter={letterGenerator()} key={j} />)

        /* These components should not be visible. I have attempted inline styling
           as well as giving them an id and targeting them in a separate CSS file,
           to no avail */
        if (j < 6) col.push(<Letter style={{ visibility: 'hidden' }} id='null' key={j}  />)
    };

    setRandomCol(col);
}, []);

return (
    <div className='columns'>
        {randomCol.map(val => val)}
    </div>
);
};
export default Column;

...next component

const Board = () => {
// Pushes the columns into an array and maps through them
const board = []
for (let i = 0; i < 8; i++) {
    board.push(<Column key={i} />)
};

return (
    <div className='main-board'>
        {board.map(col => col)}
    </div>
);
};
export default Board;

All of this works fine, minus the conditional 'visibility' styling on specific letter components. As per the game-play mechanics, I will need to be able to render more letters as 'null' and remove them from the view of the board based on user interaction in the future, as well. I'm not specifically focused on that aspect as of now, but a push in the right direction of how to even begin thinking about this problem would be greatly appreciated.

Upvotes: 1

Views: 675

Answers (1)

Drew Reese
Drew Reese

Reputation: 203099

The question seems to be mostly about the Column component. Passing a style prop to Letter doesn't work unless you then also pass it through to some DOMNode, i.e. div.

It's anti-pattern in React to store JSX in state. You should instead store the data as state and map the state to the JSX you want to render.

Example refactor:

const vowels = ['A', 'E', 'I', 'O', 'U', 'Y'];
const consonants = [
  'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L',
  'M', 'N', 'P', 'R', 'S', 'T', 'V', 'W'
];
const goldConsonants = ['X', 'Z', 'Q'];

const randomGen = () => Math.floor(Math.random() * 100);

const letterGenerator = () => {
  // Generates a random letter that letter gets passed into my letter component
  const randomNum = randomGen();

  if (randomNum >= 40) {
    return consonants[Math.floor((Math.random() * consonants.length))];
  } else if (randomNum >= 5 && randomNum < 40) {
    return vowels[Math.floor((Math.random() * vowels.length))];
  }
  return goldConsonants[Math.floor((Math.random() * goldConsonants.length))];
};

const Column = () => {
  // Generate an array of 9 random letters
  const [randomCol, setRandomCol] = useState(() => {
    return Array.from({ length: 9 }, letterGenerator);
  });
  
  return (
    <div className='columns'>
      {randomCol.map((letter, i) => ( // <-- map letters to Letter components
        <Letter key={i} letter={letter} hidden={i < 6} />
      ))}
    </div>
  );
};

In the Letter component use the passed hidden prop to conditionally apply a classname or style prop value to.

// This component takes in a random letter and sends the
// letter to a form input element when clicked
const Letter = ({ hidden, letter }) => {
  ...

  return (
    <div
      className={
        [
          'letters',
          hidden ? "hidden" : null // conditionally apply class
        ]
          .filter(Boolean)
          .join(" ")
      }
      disabled={clicked}
      onClick={() => {
        setClicked((clicked) => !clicked);
        setCurrLetter(letter);
      }}
    >
      {letter}
    </div>
  );
};

...

.hidden {
  visibility: hidden;
}

Upvotes: 1

Related Questions