Passing Builder.io Structural data as props for Custom Component

Hello Builder.io Support Team,

I am experiencing an issue with a custom React component integrated into Builder.io. The component consists of a search bar and an accordion list. However, when I bind data to the component in Builder.io, the search bar repeats for each item in the list instead of appearing just once at the top.

Here are the details:

Component Structure:

  1. Search Component (Search.jsx): Handles search input and filters items based on the search term.
  2. Accordion List Component (AccordionList.jsx): Renders a list of items passed as props.
  3. Parent Component (AccordionWithSearch.jsx): Manages the state, renders the Search component and the AccordionList component, and passes the necessary props.
// search component
import React, { useState } from 'react';
import { TextField } from '@mui/material';

const Search = ({ onFilter }) => {
  const [searchTerm, setSearchTerm] = useState('');

  const handleSearch = (event) => {
    const value = event.target.value.toLowerCase();
    setSearchTerm(value);
    onFilter(value);
  };

  return (
    <div className="search-container">
      <TextField
        label="Search"
        variant="outlined"
        fullWidth
        margin="normal"
        value={searchTerm}
        onChange={handleSearch}
      />
    </div>
  );
};

export default Search;

AccordionList.jsx:

import React from 'react';
import { Accordion, AccordionSummary, AccordionDetails, Typography } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';

const AccordionList = ({ items }) => {
  return (
    <div className="bg-white p-5">
      {items.length > 0 ? (
        items.map((item, index) => (
          <Accordion key={index}>
            <AccordionSummary expandIcon={<ExpandMoreIcon />}>
              <Typography>{item.title}</Typography>
            </AccordionSummary>
            <AccordionDetails>
              <Typography>{item.content}</Typography>
            </AccordionDetails>
          </Accordion>
        ))
      ) : (
        <Typography variant="h6">No Items Found</Typography>
      )}
    </div>
  );
};

export default AccordionList;

AccordionWithSearch.jsx

import React, { useState, useEffect } from 'react';
import { Builder } from '@builder.io/react';
import Search from './Search';
import AccordionList from './AccordionList';

const AccordionWithSearch = ({ items = [] }) => {
  const [filteredItems, setFilteredItems] = useState(items);

  useEffect(() => {
    console.log('Initial items:', items);
  }, [items]);

  const handleFilter = (searchTerm) => {
    console.log('Search term:', searchTerm);
    const newFilteredItems = searchTerm
      ? items.filter(item =>
          item.title.toLowerCase().includes(searchTerm) ||
          item.content.toLowerCase().includes(searchTerm)
        )
      : items;
    console.log('Filtered items:', newFilteredItems);
    setFilteredItems(newFilteredItems);
  };

  return (
    <div>
      <Search onFilter={handleFilter} />
      <AccordionList items={filteredItems} />
    </div>
  );
};

// Register the component with Builder.io
Builder.registerComponent(AccordionWithSearch, {
  name: 'AccordionWithSearch',
  inputs: [
    {
      name: 'items',
      type: 'list',
      subFields: [
        {
          name: 'title',
          type: 'string',
          required: true,
        },
        {
          name: 'content',
          type: 'string',
          required: true,
        },
      ],
    },
  ],
});

export default AccordionWithSearch;

I have created a data model. Here are the API results. I am trying to connect the data from the visual editor.

{
  "results": [
    {
      "lastUpdatedBy": "sReRVmTVQafbBfvBu8ZuFfFYHDi1",
      "folders": [],
      "data": {
        "policies": [
          {
            "title": "Policy 1",
            "content": "About the policy"
          },
          {
            "title": "Policy 2",
            "content": "About the policy"
          },
          {
            "title": "Policy 3",
            "content": "About the policy"
          },
          {
            "title": "Policy 4",
            "content": "About the policy"
          },
          {
            "title": "Policy 5",
            "content": "About the policy"
          },
          {
            "title": "Policy 6",
            "content": "About the policy"
          },
          {
            "title": "Policy 6",
            "content": "About the policy"
          },
          {
            "title": "Policy 8",
            "content": "About the policy"
          }
        ]
      },
      "modelId": "b9446e7297f548c99c48ba94b848620e",
      "query": [],
      "published": "published",
      "firstPublished": 1718201191076,
      "testRatio": 1,
      "lastUpdated": 1718201191133,
      "createdDate": 1718201135132,
      "createdBy": "sReRVmTVQafbBfvBu8ZuFfFYHDi1",
      "meta": {
        "kind": "data",
        "lastPreviewUrl": ""
      },
      "variations": {},
      "name": "Test Policies",
      "id": "3084699391214e219613877eaff779f1",
      "rev": "p5bkuac4q2"
    }
  ]

Hi @Kalyan1 Welcome to Builder.io Forum!

In order for me to be able to help you, could you please provide me with the Builder Content Entry link where you’re experiencing an issue this will further my investigation.

Here is how to find it:

Thanks in advance!

@garima any luck with this?

@kalyan

From your description, it sounds like the AccordionWithSearch component instance is somehow getting duplicated within the list rendering context rather than as separate and distinct elements. This could happen if the AccordionWithSearch component is being rendered within a data-binding context that inherently repeats for each item.

To ensure that the AccordionWithSearch component operates correctly with only one search bar and multiple list items, we need to ensure the following:

  1. Possibility of Misplaced Rendering Contexts: Ensure that the AccordionWithSearch component is not unintentionally nested inside a repeated context.
  2. Data Fetch and Bind Only Once for the Item List: Make sure that the items are fetched and set in the parent component correctly.

Here’s a step-by-step approach to ensure everything is correctly set up:

  1. Fetch Data Correctly and Bind it to the Component:
  2. Ensure the Parent Component Manages State and Filtering Logic Appropriately:
  3. Properly Register Your Component with Builder.io:
  4. Ensure Data Model Integration in Builder.io:

Revised Steps and Code

Step 1: Ensure Correct Data Fetching and Binding

AccordionWithSearch.jsx:

Ensure the data from your API or model is fetched only once and set to the component state correctly:

// AccordionWithSearch.jsx
import React, { useState, useEffect } from 'react';
import { Builder } from '@builder.io/react';
import Search from './Search';
import AccordionList from './AccordionList';

const AccordionWithSearch = ({ items = [] }) => {
  const [filteredItems, setFilteredItems] = useState(items);

  // Assuming items are an array of objects with title and content properties.
  useEffect(() => {
    setFilteredItems(items);
  }, [items]);

  const handleFilter = (searchTerm) => {
    const newFilteredItems = searchTerm
      ? items.filter(item =>
          item.title.toLowerCase().includes(searchTerm) ||
          item.content.toLowerCase().includes(searchTerm)
        )
      : items;
    setFilteredItems(newFilteredItems);
  };

  return (
    <div>
      <Search onFilter={handleFilter} />
      <AccordionList items={filteredItems} />
    </div>
  );
};

// Register the component with Builder.io
Builder.registerComponent(AccordionWithSearch, {
  name: 'AccordionWithSearch',
  inputs: [
    {
      name: 'items',
      type: 'list',
      subFields: [
        {
          name: 'title',
          type: 'string',
          required: true,
        },
        {
          name: 'content',
          type: 'text',
          required: true,
        },
      ],
    },
  ],
});

export default AccordionWithSearch;

Step 2: Ensure Proper Parent-Child Rendering

Search.jsx:

The Search component remains the same as it is correctly implemented for capturing and handling the search term.

AccordionList.jsx:

The Accordion List component stays the same as well since it handles rendering the list items.

Step 3: Integrate the Data

In Builder.io Integration:

Ensure your root component fetches the data correctly without grouping under multiple contexts that might cause duplication.
Ensure you are pulling the data model correctly into the Builder.io context:

// In your Builder.io Editor context
import { useState, useEffect } from 'react';

const YourComponentPage = () => {
  const [policies, setPolicies] = useState([]);

  // Fetch data from your API or Builder.io data models
  useEffect(() => {
    fetch('https://content.builder.io/api/v1/query/test_policies?apiKey=YOUR_PUBLIC_API_KEY') // adjust URL and method if needed
      .then(res => res.json())
      .then(data => setPolicies(data.results[0]?.data.policies || []));
  }, []);

  return <AccordionWithSearch items={policies} />;
};

Note: Replace 'https://content.builder.io/api/v1/query/test_policies?apiKey=YOUR_PUBLIC_API_KEY' with your actual endpoint for fetching data from Builder.io.

Troubleshoot and Verification:

  1. Check Console: Ensure no errors in the console that indicate issues with the fetch or operation.
  2. Debug Render: Ensure in AccordionWithSearch is not nested in a way it could cause repeated renders unintentionally.
  3. Inspect Builder.io Config: Ensure within Builder.io, data fetching or binding approaches should not wrap the component render multiple times.

Conclusion

By making sure that the data binding occurs only once at the top level and ensuring the data fetching occurs appropriately, you will avoid issues where components like the search bar render multiple times within the same component due to grouping in repeatable contexts.

Consider always reviewing binding contexts and state management to make sure data isn’t caught in unintended repeated renders and isn’t influenced by related structures or binding layers in the Builder editor.