Search code examples
javascriptreactjsif-statementcomponentsreact-typescript

ReactJs: How to pass data from one component to another?


If two or more cards from a component are selected how to pass data to a button component?

I have a landing page that holds two components; Template list and a button.

<TemplateList templates={templates} />

                <MenuButton style={actionButton} onClick={onOnboardingComplete}>
                    Select at least 2 options
                </MenuButton>

The TemplateList brings in an array of information for my Template and creates Template Cards with it. I am then able to select my cards and unselect them.

My Button when pressed just takes me to the next step of the onboarding.

I would like to know how I should approach linking these two components. My button now is gray and would like this to happen:

1. Button is gray and user cant go on to next step, until selecting two or more cards.

2. When two or more cards are selected, button turn blue and they are able to press the button to continue.

This is my Template List:

export type Template = {
    title: string;
    description: string;
    imgURL: string;
    id?: number;
};

type Props = {
    templates: Template[];
};

const TemplateList = ({ templates }: Props) => {
    return (
        <div className={styles.scrollContainer}>
            {templates.map((item) => (
                <TemplateCard
                    title={item.title}
                    description={item.description}
                    img={item.imgURL}
                    classNameToAdd={styles.cardContainer}
                    key={item.id}
                />
            ))}
        </div>
    );
};

export default TemplateList;

And this my Template Card:



type Props = {
    title: string;
    description: string;
    img: string;
    classNameToAdd?: string;
    classNameOnSelected?: string;
};

const TemplateCard = ({ title, description, img, classNameToAdd, classNameOnSelected }: Props) => {
    const { aspectRatio, vmin } = useWindowResponsiveValues();
    let className = `${styles.card} ${classNameToAdd}`;

    const [selected, setSelected] = useState(false);

    const handleClick = () => {
        setSelected(!selected);
    };

    if (selected) {
        className += `${styles.card} ${classNameToAdd} ${classNameOnSelected}`;
    }
    return (
        <div style={card} className={className} onClick={handleClick}>
            <img style={imageSize} src={img}></img>
            <div style={cardTitle}>
                {title}
                {selected ? <BlueCheckIcon style={blueCheck} className={styles.blueCheck} /> : null}
            </div>
            <div style={descriptionCard}>{description}</div>
        </div>
    );
};

TemplateCard.defaultProps = {
    classNameOnSelected: styles.selected,
};

export default TemplateCard;

This is how it looks like now. enter image description here


Solution

  • There are at least 3 ways to implement your requirement:

    OPTION 1

    1. Put Button component inside the <TemplateList> component

    2. Add one useState tied to <TemplateList> component to hold the number of selected cards

    3. Add two new props onSelectCard and onDeselectCard to <TemplateCard> to increment/decrement newly created state by 1 for each selected/deselected item

    4. Implement callback functions inside <TemplateList component (code below)

    5. Call onSelectCard inside <TemplateCard> when needed (code below)

    TemplateList Component

    const TemplateList = ({ templates }: Props) => {
      const [noOfSelectedCards, setNoOfSelectedCards] = useState(0);
    
      handleSelect = () => setNoOfSelectedCards(noOfSelectedCards + 1);
      handleDeselect = () => setNoOfSelectedCards(noOfSelectedCards - 1);
    
      return (
        <div classNae="template-list-container">
          <div className={styles.scrollContainer}>
            {templates.map((item) => (
              <TemplateCard
                title={item.title}
                description={item.description}
                img={item.imgURL}
                classNameToAdd={styles.cardContainer}
                key={item.id}
                onSelectCard={handleSelect}
                onDeselectCard={handleDeselect}
              />
            ))}
          </div>
          <MenuButton style={actionButton} onClick={onOnboardingComplete} className={noOfSelectedCards === 2 ? 'active' : ''}>
            Select at least 2 options
          </MenuButton>
        </div>
      );
    };

    TemplateCard Component

    const TemplateCard = ({ ..., onSelectCard, onDeselectCard }: Props) => {
      ...
    
      const [selected, setSelected] = useState(false);
    
      const handleClick = () => {
        if(selected) {
          onDeselectCard();
        } else {
          onSelectCard();
        }
        
        setSelected(!selected);
      };
    
      ...
    };

    Now, you'll have the current number of selected cards in your state noOfSelectedCards (<TemplateList> component) so you can conditionally render whatever className you want for your button or do something else with it.

    OPTION 2

    Using React Context to share state between components

    It's okay to use React Context for such cases, but if your requirements contains other similar cases for handling/sharing states between components across the app, I suggest you take a look at the 3rd option.

    OPTION 3

    Using State Management like Redux to handle global/shared states between components.

    This is probably the best option for projects where sharing states across the app is quite common and important. You'll need some time to understand concepts around Redux, but after you do that I assure you that you'll enjoy working with it.