Switching to Styled Components

Matti Bar-Zeev - Oct 29 '21 - - Dev Community

In this post join me as I switch a simple component, using a conventional import of .scss for styling, to start utilizing Styled Components.
As always, my example will be conducted on my WordSearch game which I’m experimenting on.

So let’s take the WordsPanel component into the lab and start messing with it :)

Here is how it looks in “play” mode (the greyed out word is an already “found” word):

Image description

And here is how it looks in “edit” mode. Notice that it has an additional input at the bottom where the player can add her new word to the game:

Image description

And for the code of this component I will just present the interesting parts which is the render function of the WordsPanel component:

return (
       <div className="words-panel">
           <ul>
               {words.map((word, i) => (
                   <li
                ...
            className={`word-item ${gameMode === ANSWERING_GAME_MODE ? 'answering-mode' : ''} ${
                           word.isFound ? 'word-found' : ''
                       }`}  
                   >
                       <span>{word.text}</span>
                       {gameMode === EDITING_GAME_MODE ? (
                           <button
                               onClick={() => {
                                   dispatch(removeWord(i));
                               }}
                           >
                               <Delete />
                           </button>
                       ) : null}
                   </li>
               ))}

               {gameMode === EDITING_GAME_MODE ? (
                   <li key="new" className="word-item">
                       <AddWord onWordAdd={(newWord) => dispatch(addWord(newWord))} />
                   </li>
               ) : null}
           </ul>
       </div>
   );
Enter fullscreen mode Exit fullscreen mode

Here is what’s going on, logic-wise, behind the styling of the component -
It first spreads the words it has on the state. For each word it checks whether it was already found by the player. If it was, then we mark it as “found” and style it accordingly, while if it wasn’t we give it the initial styling. If the game is in answering mode, then the words are styled accordingly as well, and the rest is plain ol’ styling..

Here is the WordsPanel.scss file content which defines the style for this component:

.words-panel {
   grid-area: wordspanel;
   width: 230px;
   list-style: none;

   .word-item {
       display: flex;
       justify-content: space-between;
       align-items: center;
       padding: 0 6px;
       margin: 6px 0px;
       border: 1px solid lightblue;
       border-radius: 5px;
       text-transform: uppercase;
       color: #53a7ea;
       height: 30px;

       span {
           pointer-events: none;
           line-height: 21px;
           user-select: none;
       }

       input {
           line-height: 21px;
           width: 80%;
           border: none;
       }

       button {
           cursor: pointer;
           background-color: transparent;
           margin: 0;
           text-align: center;
           text-decoration: none;
           display: inline-block;
           border: none;
           color: #53a7ea;
           &:disabled {
               color: lightgray;
               cursor: initial;
           }
       }

       &.answering-mode {
           &:hover {
               background-color: #53a7ea;
               color: white;
           }
       }

       &.word-found {
           background-color: grey;
           pointer-events: none;
           color: white;
       }
   }

}
Enter fullscreen mode Exit fullscreen mode

So I think I’m ready to start migrating this one to start using Styled Components.

First I’m adding the Styled Components dependency to the project, but running npm install --save styled-components

Then I import the “styled” module from the package (while commenting out the scss file cause “I also like to live dangerously” A.Powers):

// import './WordsPanel.scss';
import styled from 'styled-components';
Enter fullscreen mode Exit fullscreen mode

As you guessed it, the component looks like a mees now.
I will create the main styled component, name it StyledWordsPanel and take the entire SCSS content of the .words-panel class and put it in there. BTW, I’m going with the styled(‘div’) syntax because it feels less “WTF?-ish” than styled.div IMO:

const StyledWordsPanel = styled('div')`
   grid-area: wordspanel;
   width: 230px;
   list-style: none;

   .word-item {
       display: flex;
       justify-content: space-between;
       align-items: center;
       padding: 0 6px;
       margin: 6px 0px;
       border: 1px solid lightblue;
       border-radius: 5px;
       text-transform: uppercase;
       color: #53a7ea;
       height: 30px;

       span {
           pointer-events: none;
           line-height: 21px;
           user-select: none;
       }

       input {
           line-height: 21px;
           width: 80%;
           border: none;
       }

       button {
           cursor: pointer;
           background-color: transparent;
           margin: 0;
           text-align: center;
           text-decoration: none;
           display: inline-block;
           border: none;
           color: #53a7ea;
           &:disabled {
               color: lightgray;
               cursor: initial;
           }
       }

       &.answering-mode {
           &:hover {
               background-color: #53a7ea;
               color: white;
           }
       }

       &.word-found {
           background-color: grey;
           pointer-events: none;
           color: white;
       }
   }
`;
Enter fullscreen mode Exit fullscreen mode

And then I will use it inside my render function, like so:

<StyledWordsPanel>
<ul>
        {words.map((word, i) => (
                   ...
        ) : null}
      </ul>
</StyledWordsPanel>
Enter fullscreen mode Exit fullscreen mode

Boom. The component looks like nothing has happened to it. All in place! Let's call it a day.
But… Wait. No, we are not there yet. I can make it much better.

First of all I still got “className” attributes on my component, which I don’t like. I will get rid of them one by one. So the first className, which was “words-panel”, I got rid of when I introduced the main styled component, sweet.
Now for the next one which is the className for each list element representing a word. I will create a styled component for it as well, extract the relevant styles from the previous StyledWordsPanel and append it here:

const StyledWordListItem = styled('li')`
   display: flex;
   justify-content: space-between;
   align-items: center;
   padding: 0 6px;
   margin: 6px 0px;
   border: 1px solid lightblue;
   border-radius: 5px;
   text-transform: uppercase;
   color: #53a7ea;
   height: 30px;

   span {
       pointer-events: none;
       line-height: 21px;
       user-select: none;
   }

   input {
       line-height: 21px;
       width: 80%;
       border: none;
   }

   button {
       cursor: pointer;
       background-color: transparent;
       margin: 0;
       text-align: center;
       text-decoration: none;
       display: inline-block;
       border: none;
       color: #53a7ea;
       &:disabled {
           color: lightgray;
           cursor: initial;
       }
   }

   &.answering-mode {
       &:hover {
           background-color: #53a7ea;
           color: white;
       }
   }

   &.word-found {
       background-color: grey;
       pointer-events: none;
       color: white;
   }
`;
Enter fullscreen mode Exit fullscreen mode

And to apply it to the markup I will simply change any instance of li to StyledWordListItem:

<StyledWordsPanel>
           <ul>
               {words.map((word, i) => (
                   <StyledWordListItem
                 ...
                       className={`word-item ${gameMode === ANSWERING_GAME_MODE ? 'answering-mode' : ''} ${
                           word.isFound ? 'word-found' : ''
                       }`}
                       onMouseUp={word.isFound ? null : onWordItemMouseUp}
                       data-word={word.text}
                   >
                       <span>{word.text}</span>
                       {gameMode === EDITING_GAME_MODE ? (
                           <button
                               onClick={() => {
                                   dispatch(removeWord(i));
                               }}
                           >
                               <Delete />
                           </button>
                       ) : null}
                   </StyledWordListItem>
               ))}

               {gameMode === EDITING_GAME_MODE ? (
                   <StyledWordListItem key="new">
                       <AddWord onWordAdd={(newWord) => dispatch(addWord(newWord))} />
                   </StyledWordListItem>
               ) : null}
           </ul>
       </StyledWordsPanel>
Enter fullscreen mode Exit fullscreen mode

Yep, looks good.
This got rid of the “word-item” className but we got a couple of conditionals there which determines how to style the list item when in “answering” mode or when the word was found. Let me convert it to use Styled Components -
You can pass props to a styled component and have it act upon these props, such as changing the styles accordingly.
I will start with the style of the component when in “answering” mode. In the “answering” game mode each list item should have a hover style. Here how I created that - on the component itself I added a new prop called “gameMode” and pass the state’s gameMode to it:

 <StyledWordListItem
    ...
    gameMode={gameMode}
>
Enter fullscreen mode Exit fullscreen mode

Now I can use this prop inside the style component declaration and act upon it. Here I’m adding the hover style only when the game mode is “answering”:

${(props) =>
       props.gameMode === ANSWERING_GAME_MODE &&
       `&:hover {
           background-color: #53a7ea;
           color: white;
       }`}
Enter fullscreen mode Exit fullscreen mode

Pretty cool. It makes more sense to put styling logic inside the Styled Component declaration and not on the component itself.

Now as for the “found” issue I will do the same thing - I will add a found prop on the Styled Component and have the styles act accordingly:

<StyledWordListItem
    ...
    gameMode={gameMode}
    isFound={word.isFound}
>
Enter fullscreen mode Exit fullscreen mode

And on the Styled Component declaration:

${(props) =>
       props.isFound &&
       `
           background-color: grey;
           pointer-events: none;
           color: white;
       `}
Enter fullscreen mode Exit fullscreen mode

Yeah, it looks better now :)
I think that that’s it for this one. I have 2 styled components with conditionals in them. Of course there is more that can be done but for the sake of this walkthrough it’s sufficient.

Here is the final component render function code:

<StyledWordsPanel>
           <ul>
               {words.map((word, i) => (
                   <StyledWordListItem
                       ...
                       gameMode={gameMode}
                       isFound={word.isFound}
                   >
                       <span>{word.text}</span>
                       {gameMode === EDITING_GAME_MODE ? (
                           <button
                               onClick={() => {
                                   dispatch(removeWord(i));
                               }}
                           >
                               <Delete />
                           </button>
                       ) : null}
                   </StyledWordListItem>
               ))}

               {gameMode === EDITING_GAME_MODE ? (
                   <StyledWordListItem key="new">
                       <AddWord onWordAdd={(newWord) => dispatch(addWord(newWord))} />
                   </StyledWordListItem>
               ) : null}
           </ul>
       </StyledWordsPanel>

And here is the Styled Components declarations:

const StyledWordsPanel = styled('div')`
   grid-area: wordspanel;
   width: 230px;
   list-style: none;
`;

const StyledWordListItem = styled('li')`
   display: flex;
   justify-content: space-between;
   align-items: center;
   padding: 0 6px;
   margin: 6px 0px;
   border: 1px solid lightblue;
   border-radius: 5px;
   text-transform: uppercase;
   color: #53a7ea;
   height: 30px;

   span {
       pointer-events: none;
       line-height: 21px;
       user-select: none;
   }

   input {
       line-height: 21px;
       width: 80%;
       border: none;
   }

   button {
       cursor: pointer;
       background-color: transparent;
       margin: 0;
       text-align: center;
       text-decoration: none;
       display: inline-block;
       border: none;
       color: #53a7ea;
       &:disabled {
           color: lightgray;
           cursor: initial;
       }
   }

   ${(props) =>
       props.isFound &&
       `
           background-color: grey;
           pointer-events: none;
           color: white;
       `}

   ${(props) =>
       props.gameMode === ANSWERING_GAME_MODE &&
       `&:hover {
           background-color: #53a7ea;
           color: white;
       }`}
`;
Enter fullscreen mode Exit fullscreen mode

As always, if you have any ideas on how to make this better or any other technique, be sure to share with the rest of us!

Cheers

Hey! If you liked what you've just read drop by to say hi on twitter :) @mattibarzeev 🍻

Photo by Dan-Cristian Pădureț on Unsplash

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .