.map is not a function with MaterialUI Tab component - reactjs

So I'm trying to populate a in-memory MirageJS' database with a post request of the data in useEffect, making a get request after. Then I save the result to columnsNames state and use it to map this array and read the information as a label of Tab MaterialUI component. However, I have some questions:
When I put a console.log after useEffect or inside get request, both response.data and columnsNames state have 2 identical arrays. Why 2 and not 1?
At runtime the console prints
Uncaught TypeError: columnsNames.map is not a function
The above error occurred in the <ScrollableTabsButtonAuto> component
and nothing is showed in the screen. Why?
Anyone that could help me, thanks in advance!
Data.ts
export const data = [
{
"id": 1,
"title": "Test 1",
"steps": [
"Read a starting book",
"Keep calm and drink coffee",
"Don't Panic so much",
]
},
{
"id": 2,
"title": "Test 2",
"steps": [
"Get some helpful help",
"Don't loose yourself",
]
},
{
"id": 3,
"title": "Test 3",
"steps": [
]
},
{
"id": 4,
"title": "Test 4",
"steps": [
]
},
{
"id": 5,
"title": "Test 5",
"steps": [
]
},
{
"id": 6,
"title": "Test 6",
"steps": [
]
}
]
App.tsx
import { createServer, Model } from 'miragejs';
createServer({
models: {
columns: Model,
},
routes() {
this.namespace = 'api'
this.get('columns', () => {
return this.schema.all('columns')
})
this.post('columns', (schema, request) => {
const data = JSON.parse(request.requestBody)
return schema.create('columns', data)
})
}
})
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Tab.tsx
interface ColumnTypes {
id: number,
title: string,
steps: string[]
}
export default function ScrollableTabsButtonAuto() {
const [value, setValue] = React.useState(0);
const [columnsNames, setColumnsNames] = React.useState<ColumnTypes[]>([])
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
};
React.useEffect(() => {
api.post('columns', data)
.then( () => {
api.get<ColumnTypes[]>('columns')
.then(response => setColumnsNames(response.data))
})
}, [])
return (
<Box sx={{ maxWidth: { xs: 320, sm: 1280 }, bgcolor: 'background.paper' }}>
<Tabs
value={value}
onChange={handleChange}
variant="scrollable"
scrollButtons="auto"
aria-label="scrollable auto tabs example"
>
{columnsNames.map(({ id, title }) => (
<Tab
label={title}
key={id}
/>)
)}
</Tabs>
</Box>
);
}

Related

Getting MUI's Autocomplete to correctly display categories and subcategories

I'm trying to essentially achieve the following image which is found here:
In that thread, they talk about the best way to display categories and subcategories and the consensus is an MUI Autocomplete.
I'm not however sure how I would achieve something like that at all and would like some help with how I could achieve it.
What I need is for the user to only be able to select one category, whether it be a "root category" or a sub-category. So in the example above, either the "Boysenberry" or the "Brulee Berry".
I also want to try and have the id of said category so I can apply it on my back end (which I'm sure I can do.
My fetched json structure looks like the below:
[
{
"id": 1,
"name": "Audio Visual Equipment",
"parent": null,
"stockItems": [],
"childCategories": [
{
"id": 2,
"name": "Projectors",
"stockItems": [],
"childCategories": [
{
"id": 3,
"name": "Lenses",
"stockItems": [],
"childCategories": []
}
]
}
]
},
{
"id": 4,
"name": "Lighting Equipment",
"parent": null,
"stockItems": [],
"childCategories": [
{
"id": 5,
"name": "Intelligent",
"stockItems": [],
"childCategories": []
},
{
"id": 6,
"name": "Generic",
"stockItems": [],
"childCategories": []
},
{
"id": 7,
"name": "Control",
"stockItems": [],
"childCategories": []
}
]
},
{
"id": 8,
"name": "Sound Equipment",
"parent": null,
"stockItems": [],
"childCategories": [
{
"id": 9,
"name": "Mixing Desk",
"stockItems": [],
"childCategories": []
}
]
},
{
"id": 10,
"name": "Cables",
"parent": null,
"stockItems": [],
"childCategories": [
{
"id": 11,
"name": "Multicore",
"stockItems": [],
"childCategories": []
},
{
"id": 12,
"name": "Lighting",
"stockItems": [],
"childCategories": []
},
{
"id": 13,
"name": "Audio",
"stockItems": [],
"childCategories": []
},
{
"id": 14,
"name": "Video",
"stockItems": [],
"childCategories": []
},
{
"id": 15,
"name": "Power",
"stockItems": [],
"childCategories": []
}
]
}
]
EDIT:-
I get the following warning when I refresh the page:
MUI: The value provided to Autocomplete is invalid.None of the options match with `-1`.You can use the `isOptionEqualToValue` prop to customize the equality test.
When I then click on the Autocomplete, I get the "root" categories only. When I then click on one, the name is not shown and I get the following error:
MUI: The value provided to Autocomplete is invalid.None of the options match with `1`.You can use the `isOptionEqualToValue` prop to customize the equality test.
1. Flattening the List
My approach is to "flatten" the list of categories into a single array so that MUI can evaluate each sub-category. Each of my flat options has a depth property so that I can display it with the correct level of indentation.
We can use the code from the Checkboxes example and add an indentation with the MUI sx prop:
renderOption={(props, option, { selected }) => (
<li {...props}>
<Checkbox checked={selected} sx={{ ml: 2 * option.depth }} />
{option.name}
</li>
)}
2. Filtering Matches
I'm assuming that we want to display the top-level category above a sub-category which matches on the sub-category term only. Like in your linked "ber" example, if the category was "Fall Gold" and the subcategory was "Fall Gold Berry". This means that we should consider the child terms when deciding if a term is a match.
To achieve this, I am including a matchTerms property on all option objects and using a custom filterOptions function on the Autocomplete which looks at this property. With the createFilterOptions utility, we just need to determine what texts to examine:
filterOptions={(createFilterOptions({
// join with some arbitrary separator to prevent matches across adjacent terms
stringify: (option) => option.matchTerms.join("//")
}))}
3. Highlighting
The last piece of this is the highlighting, which is not included in MUI. The MUI docs recommend the autosuggest-highlight package and include an example of how to use it. We can copy that, changing option.title to option.name.
Complete Code
JavaScript
import {
Autocomplete,
TextField,
Checkbox,
createFilterOptions
} from "#mui/material";
import { data } from "./data";
import parse from "autosuggest-highlight/parse";
import match from "autosuggest-highlight/match";
const toOptions = (category, depth = 0, parentId = null) => {
const { id, name, childCategories = [] } = category;
const children = childCategories.flatMap((child) =>
toOptions(child, depth + 1, id)
);
const option = {
id,
name,
depth,
parentId,
matchTerms: [name].concat(children.map((obj) => obj.name))
};
return [option].concat(children);
};
const optionsList = data.flatMap((category) => toOptions(category));
export default () => {
return (
<Autocomplete
options={optionsList}
getOptionLabel={(option) => option.name}
renderOption={(props, option, { selected, inputValue }) => {
const matches = match(option.name, inputValue);
const parts = parse(option.name, matches);
return (
<li {...props}>
<Checkbox checked={selected} sx={{ ml: 2 * option.depth }} />
<div>
{parts.map((part, index) => (
<span
key={index}
style={{
fontWeight: part.highlight ? 700 : 400
}}
>
{part.text}
</span>
))}
</div>
</li>
);
}}
renderInput={(params) => <TextField {...params} />}
filterOptions={createFilterOptions({
// join with some arbitrary separator to prevent matches across adjacent terms
stringify: (option) => option.matchTerms.join("//")
})}
/>
);
};
TypeScript
import {
Autocomplete,
TextField,
Checkbox,
createFilterOptions
} from "#mui/material";
import { data } from "./data";
import parse from "autosuggest-highlight/parse";
import match from "autosuggest-highlight/match";
// describes the input data
type Category = {
id: number;
name: string;
childCategories?: Category[];
};
// describes the format that we want
interface Option {
id: number;
name: string;
depth: number;
parentId: number | null;
matchTerms: string[];
}
const toOptions = (
category: Category,
depth: number = 0,
parentId: number | null = null
): Option[] => {
const { id, name, childCategories = [] } = category;
const children = childCategories.flatMap((child) =>
toOptions(child, depth + 1, id)
);
const option = {
id,
name,
depth,
parentId,
matchTerms: [name].concat(children.map((obj) => obj.name))
};
return [option].concat(children);
};
const optionsList: Option[] = data.flatMap((category) => toOptions(category));
export default () => {
return (
<Autocomplete
options={optionsList}
getOptionLabel={(option) => option.name}
renderOption={(props, option, { selected, inputValue }) => {
const matches = match(option.name, inputValue);
const parts = parse(option.name, matches);
return (
<li {...props}>
<Checkbox checked={selected} sx={{ ml: 2 * option.depth }} />
<div>
{parts.map((part, index) => (
<span
key={index}
style={{
fontWeight: part.highlight ? 700 : 400
}}
>
{part.text}
</span>
))}
</div>
</li>
);
}}
renderInput={(params) => <TextField {...params} />}
filterOptions={createFilterOptions({
// join with some arbitrary separator to prevent matches across adjacent terms
stringify: (option) => option.matchTerms.join("//")
})}
/>
);
};
CodeSandbox Link

making dynamic route by API with next+react

i'd like to make a dynamic route with react and next.js
I have an API like this.
[
{
"id": 1,
"name": "books",
"pages": "277",
"title": "Book A",
"no": 88
},
{
"id": 2,
"name": "books",
"pages": "90",
"title": "Book B",
"no": 32
},
{
"id": 4,
"name": "books",
"pages": "184",
"title": "Book C",
"no": 89
}
...
]
and this API will show in the screen like this. this is mapped li in Book.jsx
<li>
<p>{book.name}</p>
<p>{book.pages}</p>
</li>
Now, what i want to make is when i click li, it connects to the "no" of API and make a new window.
so I created [bookNo].jsx and the same location with pages > index.jsx and added these to [bookNo].jsx
const DetailedView = () => {
const router = useRouter();
const {bookNo} = router.query;
return (
<div>
<p>{bookId}</p>
</div>
);
};
to make router works, i added these code to Book.jsx
useEffect(() => {
getBooks().then(res => {
const book = res.data.DATA.RESPONSE.BOOKS;
for (let i = 0; i < book.length; i++) {
const bookNo = bookData[i].no;
}
}).catch(e => {
console.log(e)
})
}, [])
<Link href="pages/bookView/[bookId]" as={`/bookView/${bookId}`}>
<li onClick>
<p>{book.name}</p>
<p>{book.pages}</p>
</li>
</Link>
And I've got 2 errors that
ReferenceError: bookNo is not defined and router is not working at all.
Can anyone help me with this problem?

React Hook useState Is Returning Undefined

I am trying to fill a table with data from an API.
UPDATED*************
import React, { useMemo, useState, useCallback, useEffect } from "react";
import {
AppLayout,
Button,
Box,
Form,
SpaceBetween,
Grid,
} from "#affn/awsui-components-react/polaris";
import "#affn/awsui-global-styles/polaris.css";
import "./styles/landing-page.scss";
import { appLayoutLabels, externalLinkProps } from "./common/labels";
import Picture1 from "./resources/engineLogos/bric_team_dark_backgroung(1).svg";
import {
ExternalLinkItem,
Navigation,
InfoLink,
} from "./commons/common-components-BRIC";
function BricPage() {
const Content = ({ navigationOpen }) => {
//Constants needed by the form ------------------------------------------------
const refreshPage = () => {
window.location.reload();
};
// The comment (i.e conversation) id must be unique
const conversationId = uuidv4();
//Function to handle submit click and create SIM
const handleClick = (
title1,
description1,
) => () => {
console.log(title1);
console.log(description1);
};
const [GetSimIDs, setGetSimIDs] = React.useState([]); //Which is the impacted region?
// Using useEffect to call the API once mounted and set the data
useEffect(() => {
window.harxny.api
.invokeProxy(
"/sit/ises?sort=createDate desc&q=status:(Open) containingFolder:(45-b5b9-4829-8b87-489053f9bb42)",
{
method: "GET",
// SIM integration is only possible from the 'beta' and 'corp' stages.
stage: "corp",
headers: {
"Content-Type": "application/json",
},
}
) //api finishes here
.then((xhr) => {
//response is captured here
//var SIMID = JSON.parse(xhr.response).id;
console.log(xhr.responseText);
const data = JSON.parse(xhr.response);
//const data = xhr.response;
console.log(data);
console.log(data.totalNumberFound);
setGetSimIDs(data);
console.log(GetSimIDs);
});
}, []);
//End of Constants -------------------------------------------------------------
console.log(GetSimIDs);
return (
<Box margin={{ bottom: "l" }}>
<div className="center-form">
<Box>
<Grid
gridDefinition={[
{
colspan: { xl: "2", l: "2", s: "5", xxs: "10" },
offset: { l: "2", xxs: "1" },
},
{
colspan: { xl: "2", l: "3", s: "5", xxs: "10" },
offset: { s: "0", xxs: "1" },
},
]}
>
<div className="custom-home-main-content-area">
<SpaceBetween size="l">
<Form
actions={
<SpaceBetween direction="horizontal" size="xs">
<Button onClick={refreshPage} variant="link">
Reset Form
</Button>
<Button
variant="primary"
onClick={handleClick(
title1,
description1,
)}
ariaLabel="Submit"
>
Submit
</Button>
</SpaceBetween>
}
>
</Form>
</SpaceBetween>
</div>
{/* Table goes here */}
{console.log(GetSimIDs)}
<tbody>
<tr>
<th>title</th>
<th>Id</th>
</tr>
{GetSimIDs.documents.map((item, i) => (
<tr key={i}>
<td>{item.title}</td>
<td>{item.id}</td>
</tr>
))}
</tbody>
</Grid>
</Box>
</div>
</Box>
);
};
const [navigationOpen, setNavigationOpen] = React.useState(false);
return (
<AppLayout
disableContentPaddings={true}
content={<Content />}
navigation={<Navigation activeHref="#/" />}
navigationOpen={navigationOpen}
onNavigationChange={({ detail }) => setNavigationOpen(detail.open)}
toolsHide={true}
ariaLabels={appLayoutLabels}
/>
);
}
export default BricPage;
The state GetSimIDs is updated successfully with data like this:
{
"documents": [
{
"assignedFolder": "4a37-416c-8531-",
"extensions": {
"tt": {
"impact": 5,
"category": "EiC",
"type": "IBug",
"item": "Macro",
"assignedGroup": "EiC",
"justification": [],
"minImpact": 5,
"status": "Assd"
}
},
"watchers": [
{ "id": "bric-primary#amazon.com", "type": "email" },
{ "id": "sssesuni#amazon.com", "type": "email" },
{ "id": "raaishwa#amazon.com", "type": "email" },
{ "id": "dipchakr#amazon.com", "type": "email" }
],
"customFields": {
"number": [{ "id": "fte_saving", "value": 0 }],
"date": [
{ "id": "delivery_date", "value": "2022-05-17T15:43:49.825Z" }
],
"string": [
{ "id": "category_of_the_request", "value": "Other" },
{ "id": "region_of_impact", "value": "NA" },
{ "id": "tool_type", "value": "Excel Macro" },
{
"id": "impacted_tool",
"value": "Tickets Helper"
}
]
}
},
{
"title": "Issue or Bug - Global Wizard - NA",
"assignedFolder": "416c-8531-37fa3a701712",
"watchers": [{ "id": "bprimary#a.com", "type": "email" }],
"customFields": {
"number": [{ "id": "fte_saving", "value": 0 }],
"date": [
{ "id": "delivery_date", "value": "2022-05-13T02:22:46.751Z" }
],
"string": [
{ "id": "category_of_the_request", "value": "Other" },
{ "id": "region_of_impact", "value": "NA" },
{ "id": "tool_type", "value": "Excel Macro" },
{ "id": "impacted_tool", "value": "Global Wizard" }
]
}
}
],
"totalNumberFound": 2,
"searchLogMessages": [],
"startToken": ""
}
So I tried to update the table with the following code:
<tbody>
<tr>
<th>title</th>
<th>Id</th>
<th>status</th>
</tr>
{GetSimIDs.map((documents, i) => (
<tr key={i}>
<td>{documents.title}</td>
<td>{documents.id}</td>
<td>{documents.status}</td>
</tr>
))}
</tbody>
But I keep getting an error in line
{GetSimIDs.map((documents, i) => (
Saying that TypeError: s is undefined
Any idea as of why it seems not getting the data from the hook?
I am very new to react so all feedback would be appreciated.
Thanks
Luis V.
This is a common problem with async data. The state is initially undefined, and is filled in later after the request has completed. There will always be at least one render before your data is loaded.
You can fix this in several ways, but the simplest would be to just initialize the state to an empty array:
const [GetSimIDs, setGetSimIDs] = React.useState([]);
Now the state is always defined, and you can map over it even before the data is loaded.
Another option would be to check the data before mapping it:
{GetSimIDs && GetSimIDs.map((documents, i) => (
Data returned from the API seems to be an object with a property documents which is an array. Either you can
setGetSimIDs(data.documents)
OR
GetSimIDs.documents.map(...)
Update (Codesandbox): Seems you're also missing a few null checks due to which there are errors. Initially the object + array are empty so we can't use the map function. Only when data has successfully loaded we can render the rows.
I have used your data set & made a mock api.
export default function App() {
const [GetSimIDs, setGetSimIDs] = useState({});
useEffect(() => {
axios
.get("https://getsimids.free.beeceptor.com/my/api/path")
.then((res) => {
setGetSimIDs(res.data);
});
}, []);
return (
<div className="App">
<table>
<tbody>
<tr>
<th>title</th>
<th>Id</th>
<th>status</th>
</tr>
{GetSimIDs.documents &&
GetSimIDs.documents.length > 0 &&
GetSimIDs.documents.map((documents, i) => (
<tr key={i}>
<td>{documents.title}</td>
<td>{documents.id}</td>
<td>{documents.status}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
First, you can log the value of GetSimIDs and GetSimIDs before the map function.
console.log(GetSimIDs, GetSimIDs.documents)
GetSimIDs.map...
It will probably be undefined as at the first load of the page it is not initialized.
I would suggest you set the const [GetSimIDs, setGetSimIDs] = React.useState([]); instead of empty.
For your further problem with react hooks (useEffect) I would suggest you setState outside the useEffect. Extract the method that fetches data e.g getData and then use it inside useEffect()
useEffect(()=>{getData()},[])

React - dynamic index of array. Giving undefined for object key

I have a component that is collecting an array of objects from .json
function DisplaySingleQuestion() {
const [ displaySingleQuestion, setDisplaySingleQuestion ] = useState([]);
const [ questionNumber, setQuestionNumber ] = useState(0);
const [currentQuestion, setCurrentQuestion ] = useState("");
useEffect(() => {
fetch("./database/questions.json").then(
async resp => await resp.json()
).then(
data => setDisplaySingleQuestion(data.questions)
)
.catch(
err => console.log(err)
)
}, []);
useEffect(() => {
setCurrentQuestion(displaySingleQuestion[questionNumber])
}, [displaySingleQuestion]);
console.log(currentQuestion.question);
return (
<div>
<p>to jest Single Question</p>
<div style={{backgroundColor: "red"}}>
{/* <p>{displaySingleQuestion[questionNumber].question}</p> */}
</div>
</div>
)
}
If I ask for a specific array index it works well.
For example: displaySingleQuestion[0].question shows the right string.
If instead I get the 0 from the state, it gives undefined.
The component above gives undefined on console.log(currentQuestion.question);
However if I run console.log(currentQuestion); it shows the object in question:
{id: 1, question: "Question text?"}
id: 1question: "Question text?"
[[Prototype]]: Object
The JSON looks like this:
{
"questions": [
{
"id": 1,
"question": "Question text?"
},
{
"id": 2,
"question": "Banan??"
},
{
"id": 3,
"question": "Cytryna?"
},
{
"id": 4,
"question": "Dynia?"
},
{
"id": 5,
"question": "Eukaliptus?"
}
]
}
Why can't I select the question key by calling displaySingleQuestion[questionNumber].question?
What am I doing wrong, and how to fix it?
Thank you

How to search and filter in array of objects on setState

I'm trying to create a search based on an array of objects with react which data is in this format:
const data = [
{"category 1" : [
{
"name": "Orange",
"desc": "juice, orange, Water"
},
{
"name": "Ananas",
"desc": "juice, ananas, water"
}
]
},
{"category 2" : [
{
"name": "Banana Split",
"desc": "Banana, ice cream, chocolat, topping",
"allergens": "nuts"
},
{
"name": "Mango Sticky Rice",
"desc": "Mango, rice, milk",
"allergens": ""
}
]
}
]
I stored this data inside useState declaration to be able to render accordingly on data chnage:
const [filteredBySearch, setFilteredBySearch] = useState(data)
I have an input where we can type anything and set inside useState declaration.
Goal:
If I type in my input:
"Jui"
Output should be:
console.log(filteredBySearch)
/* output:
[
{"category 1" : [
{
"name": "Orange",
"desc": "juice, orange, Water"
},
{
"name": "Ananas",
"desc": "juice, ananas, water"
}
]
},
{"category 2" : []
}
]*/
Exemple 2:
If I type in my input:
"Orange banana"
Output should be:
console.log(filteredBySearch)
/* output: [
{"category 1" : [
{
"name": "Orange",
"desc": "juice, orange, Water"
}
]
},
{"category 2" : [
{
"name": "Banana Split",
"desc": "Banana, ice cream, chocolat, topping",
"allergens": "nuts"
}
]
}
]*/
I've try creating a new object with map and filter and set it with setFilteredBySearch, but I can't get anything, even creating this new object.
This the full component:
import Card from '../components/Card'
import React, { useState } from 'react';
export default function IndexPage({ data, search }) {
//search is the result of input value set on a useState
//Filter categoriesFoods by search
const [FilteredBySearch, setFilteredBySearch] = useState(data)
return (
<div className="main-content">
<div className="card-container">
{
FilteredBySearch.map(function(el, i) {
return (
<div key={i}>
<h2 className="category" id={Object.keys(el)}>{Object.keys(el)}</h2>
{
el[Object.keys(el)].map (function(itm,index){
return <Card key={index} infoItem={itm}/>
})
}
</div>
)
})
}
</div>
<style jsx>{`...`}</style>
</div>
)}
Any idea for me ?
Thanks a lot for your guidance!
I think this is what you are looking for. I have created below utilities for filtering as per your requirement.
const dataObj = [
{
'category 1': [
{
name: 'Orange',
desc: 'juice, orange, Water',
},
{
name: 'Ananas',
desc: 'juice, ananas, water',
},
],
},
{
'category 2': [
{
name: 'Banana Split',
desc: 'Banana, ice cream, chocolat, topping',
allergens: 'nuts',
},
{
name: 'Mango Sticky Rice',
desc: 'Mango, rice, milk',
allergens: '',
},
],
},
]
const checkIfInputMatches = (input, desc) => input.toLowerCase().split(" ").some(o => desc.toLowerCase().includes(o))
const filterByInput = (data, input) => {
let finalResult = [];
data.forEach(d => {
let keys = Object.keys(d);
let values = Object.values(d);
finalResult = [...finalResult, ...values.map((obj, index) => {
let result = obj.filter(o => checkIfInputMatches(input, o.desc))
return {[keys[index]]: result}
})]
})
return finalResult
}
console.log(filterByInput(dataObj, 'JUI'))
console.log(filterByInput(dataObj, "orange"))
console.log(filterByInput(dataObj, "rice"))
console.log(filterByInput(dataObj, "Orange banana"))
Hope this helps.

Resources