Mapping An Array In React JS not rendering - reactjs

I have a nested dictionary object called teams which I preprocess into an array of arrays.
Initially, my teams data (the nested array) looks like this:
and then it is processed into a teamCards array which looks like this:
However even once processed, my map function is still not mapping my array into a component like I would like. Does anyone know why not? Here is my react code:
import React, {useEffect} from 'react'
import { Grid } from '#material-ui/core'
import TeamCard from './TeamCard'
import loader from '../images/loader.gif'
export default function Teams({teamsLoading, teams}) {
console.log(teams)
const teamCards = []
function populateTeamCards() {
Object.keys(teams).forEach(function(key) {
Object.keys(teams).forEach(function(key) {
Object.keys(teams[key]).forEach(function(t) {
teamCards.push([t, teams[key][t]])
})
})
})
}
useEffect(() => {
if (teamsLoading == false) {
populateTeamCards()
}
}, [teamsLoading])
return (
teamsLoading ?
<img src={loader} alt="loading..." /> :
<Grid>
{teamCards.map((element, index) => {
return (
<TeamCard
key={index}
teamName={element[0]}
/>
)
})}
</Grid>
)
}

You can try this
import React, { useEffect, useState } from "react";
import { Grid } from "#material-ui/core";
import TeamCard from "./TeamCard";
import loader from "../images/loader.gif";
export default function Teams({ teamsLoading, teams }) {
const [teamCards, setTeamCards] = useState([]);
function populateTeamCards() {
let newArray = [];
Object.keys(teams).forEach(function (key) {
Object.keys(teams[key]).forEach(function (t) {
newArray.push([t, teams[key][t]]);
});
});
setTeamCards(newArray);
}
useEffect(() => {
if (teamsLoading == false) {
populateTeamCards();
}
}, [teamsLoading]);
return teamsLoading ? (
<img src={loader} alt="loading..." />
) : (
<Grid>
{teamCards.map((element, index) => {
return <TeamCard key={index} teamName={element[0]} />;
})}
</Grid>
);
}

You're setting an instance variable's value and this does not trigger a component re-render, which means even after teamCards is updated, the UI still stays the same as when it was empty.
What you need is to use a React state like this:
const [teamCards, setTeamCards] = useState([]);
...
const cards = [];
// ... push to cards ...
setTeamCards(cards);

Related

Is there a way to split a i18n translation?

OK, so I got this component that animating my titles. But know, I want to translate my application with i18n, but problem is, I was using .split() function to make an array of words of my titles, I know that .split() is taking only string, and all I tested return me a JSX Element. So I can't split my pages title.
Is there another way to do it, to keep my translation ?
Here is an exemple of my pages with the component title and what I tried (I also tried with Translation from react-i18next, but same result)
About.tsx
import { useEffect, useState } from "react";
import AnimatedLetters from "../AnimatedLetters/AnimatedLetters"
import { Div } from "../Layout/Layout.elements";
import { useTranslation, Trans } from "react-i18next";
const About = () => {
const [letterClass, setLetterClass] = useState<string>('text-animate');
const { t, i18n } = useTranslation();
useEffect(() => {
setTimeout(() => {
setLetterClass('text-animate-hover')
}, 3000)
}, [])
const getTranslation = (value: string) => {
return <Trans t={t}>{value}</Trans>;
}
return (
<Div>
<div className="container about-page">
<div className="text-zone">
<h1>
<AnimatedLetters
strArray={getTranslation('About.Title').split("")}
idx={15}
letterClass={letterClass}
/>
</h1>
</div>
</Div>
)
}
export default About
Before decide to translate, I was making like that :
<AnimatedLetters
strArray={"About us".split("")}
idx={15}
letterClass={letterClass}
/>
AnimatedLetters.tsx
import { FunctionComponent } from "react"
import { Span } from "./AnimatedLetters.elements"
type Props = {
letterClass: string,
strArray: any[],
idx: number
}
const AnimatedLetters: FunctionComponent<Props> = ({ letterClass, strArray, idx }) => {
return (
<Span>
{
strArray.map((char: string, i: number) => (
<span key={char + i} className={`${letterClass} _${i + idx}`} >
{char}
</span>
))
}
</Span>
)
}
export default AnimatedLetters
OK I found it! I put the solution here in the case of someone else needs it!
In fact there is two ways, don't forget that I needed an array to transmet to my component, so the first was to put directly an array into my translations json files, like:
common.json
{
"Title": ["A","b","o","u","t","","u","s"]
}
But i did not thought that was very clean.
So the second way was to create a method that tooks the key of the json file, to return it directly, like this :
const getTranslate = (value: string) => {
return (`${t(value)}`)
}
Then I stringify it to can use .split() to make an array
const getTranslate = (value: string) => {
return JSON.stringify(`${t(value)}`).split("")
}
The translate and the array worked nicely, but it returned with double quotes. The last thing was to erase it, with a replace and a little regex, and now : everything works like a charm 😁
All the component looks like it now :
About.tsx
import { useEffect, useState } from "react";
import AnimatedLetters from "../AnimatedLetters/AnimatedLetters"
import { Div } from "../Layout/Layout.elements";
import { useTranslation } from 'react-i18next';
const About = () => {
const [letterClass, setLetterClass] = useState('text-animate');
const { t } = useTranslation();
useEffect(() => {
setTimeout(() => {
setLetterClass('text-animate-hover')
}, 3000)
}, [])
const getTranslate = (value: string) => {
return JSON.stringify(`${t(value)}`).replace(/\"/g, "").split("")
}
return (
<Div>
<div className="container about-page">
<div className="text-zone">
<h1>
<AnimatedLetters
strArray={getTranslate('Title')} // <<--- Called here
idx={15}
letterClass={letterClass}
/>
</h1>
</div>
</div>
</Div>
)
}
export default About

How to use state on one element of map on typescript?

I want to use onClick on one element of my map and set "favorite" for it. Basically, I'm trying to change the SVG of a Icon to the filled version, but with the map, all of items are changing too.
I already try to pass this to onClick, but doesn't work.
My code:
import React, { Component, useState, useEffect } from "react";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import { ForwardArrow } from "../../../assets/images/ForwardArrow";
import { BackArrow } from "../../../assets/images/BackArrow";
import * as S from "./styled";
import { IconFavoriteOffer } from "../../../assets/images/IconFavoriteOffer";
import { Rating } from "../../../assets/images/Rating";
import { TruckFill } from "../../../assets/images/TruckFill";
import { OpenBox } from "../../../assets/images/OpenBox";
import { IconCartWht } from "../../../assets/images/IconCartWht";
import axios from "axios";
import { off } from "process";
import SwitcherFavorite from "../SwitcherFavorite";
export default function Carousel() {
const [offers, setOffers] = useState<any[]>([]);
useEffect(() => {
axios.get("http://localhost:5000/offers").then((response) => {
setOffers(response.data);
});
}, []);
const [favorite, setFavorite] = useState(true);
const toggleFavorite = () => {
setFavorite((favorite) => !favorite);
};
return (
<>
<Slider {...settings}>
{offers.map((offer, index) => {
return (
<S.Offer key={index}>
<>
<S.OfferCard>
<S.OfferCardTop>
<S.OfferContentTop>
<S.OfferFavorite>
<S.OfferFavoriteButton onClick={toggleFavorite}> // Want to get this element of mapping
<SwitcherFavorite favorite={favorite} />
</S.OfferFavoriteButton>
</S.OfferFavorite>
<S.OfferStars>
<Rating />
</S.OfferStars>
</S.OfferContentTop>
</S.OfferCardTop>
</S.OfferCard>
</>
</S.Offer>
);
})}
</Slider>
</>
);
}
So, how can I do it?
Instead of using a single boolean flag with your current [favorite, setFavorite] = useState(false) for all the offers, which wouldn't work, you can store the list of offer IDs in an array. In this way you can also have multiple favourited offers.
Assuming your offer item has a unique id property or similar:
// This will store an array of IDs of faved offers
const [favorite, setFavorite] = useState([]);
const toggleFavorite = (id) => {
setFavorite((previousFav) => {
if (previousFav.includes(id)) {
// remove the id from the list
// if it already existed
return previousFav.filter(favedId => favedId !== id);
}
// add the id to the list
// if it has not been here yet
return [...previousFav, id]);
}
};
And then in your JSX:
/* ... */
<S.OfferFavoriteButton onClick={() => toggleFavorite(offer.id) }>
<SwitcherFavorite favorite={favorite.includes(offer.id)} />
// Similar to your original boolean flag to switch icons
</S.OfferFavoriteButton>
/* ... */

Child Component doesn't rerender when state of parent component changes

I have the following issue: I have an Component that renders other components in it. One of this component gets state variables of my parent component as parameter and are using them actively, but they don't rerender when the state of the parent component changes. Another problem that I am facing is that I have an additional item in my list that navigates that is activated when the user has a special roleID. The changing of the state works completely fine, but in this situation the additional item only gets visible after I changed the path param of my url.
parent component:
import React, { useEffect, useState } from 'react';
import {Row, Col} from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import '../../App.css';
import ProfileSettings from './profileSettings';
import SettingsChooser from './settingsChooser';
// import SettingRoutings from '../settingRoutings';
import {BrowserRouter as Router, useHistory, useLocation, useParams} from 'react-router-dom';
// import Routings from '../Routings.js';
import UserRequests from './userRequests';
import useAuth from '../../API/useAuthentification';
import { CONTROLLERS, useBackend } from '../../hooks/useBackend';
function UserSettings({user}) {
const {title: path} = useParams();
const [acst, setAcst] = useState(localStorage.accessToken);
const [rft, setRft] = useState(localStorage.refreshToken);
const history = useHistory();
const [items, setItems] = useState(['Profile', 'Requests','Log Out', 'Delete Account']);
const [authError, setAuthError] = useState(false);
const [userValues, authentificate] = useBackend(authError, setAuthError, user);
const [component, setComponent] = useState(<></>);
const [defaultItem, setDefaultItem] = useState(0);
useEffect(() => {
console.log('render');
authentificate(CONTROLLERS.USERS.getUserByAccessToken());
}, [acst, rft]);
window.addEventListener('storage', () => localStorage.accessToken !== acst ? setAcst(localStorage.accessToken) : '');
window.addEventListener('storage', () => localStorage.refreshToken !== rft ? setRft(localStorage.refreshToken) : '');
useEffect(() => {
if(userValues?.roleID === 1) {
items.splice(0, 0, 'Admin Panel');
setItems(items);
}
console.log(items);
}, [userValues]);
useEffect(() => {
// if(path==='logout') setDefaultItem(2);
// else if(path==='deleteAccount') setDefaultItem(3);
// else if(path==='requests') setDefaultItem(1);
}, [])
const clearTokens = () => {
localStorage.accessToken = undefined;
localStorage.refreshToken = undefined;
}
useEffect(() => {
console.log(path);
if(path ==='logout' && !authError) {
setDefaultItem(2);
clearTokens();
}
else if(path === 'deleteaccount') {
setDefaultItem(3);
if(userValues?.userID && !authError) {
authentificate(CONTROLLERS.USERS.delete(userValues.userID));
}
clearTokens();
history.push('/movies/pages/1');
}
else if(path==='requests') {
setDefaultItem(1);
setComponent(<UserRequests user={userValues} setAuthError={setAuthError} authError={authError}/>);
} else {
setComponent(<ProfileSettings user={userValues} setAuthError={setAuthError} authError={authError}/>);
}
}, [path]);
useEffect(() => {
console.log(defaultItem);
}, [defaultItem])
return (
<div >
<Row className="">
<Col className="formsettings2" md={ {span: 3, offset: 1}}>
<SettingsChooser items={items} headline={'Your Details'} defaultpath='userSettings' defaultactive={defaultItem} />
</Col>
<Col className="ml-5 formsettings2"md={ {span: 6}}>
{authError ? <p>No Access, please Login first</p> : component}
</Col>
</Row>
</div>
);
}
export default UserSettings;
Child component (settingsChooser):
import React, {useEffect, useState} from 'react';
import {Card, Form, Button, Nav, Col} from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import { LinkContainer } from 'react-router-bootstrap';
import '../../App.css'
function SettingsChooser({items, headline, defaultpath, defaultactive}) {
const [selected, setSelected] = useState(defaultactive);
const handleClick = (e, key) => {
setSelected(key);
}
useEffect(() => console.log("rerender"), [items, defaultactive]);
useEffect(() => {
setSelected(defaultactive);
}, [])
return(
<>
<Card className="shadow-sm">
<Card.Header className="bg-white h6 ">{headline}</Card.Header>
{items.map((item, idx) =>{
return(
<LinkContainer to={`/${defaultpath}/${(item.replace(/\s/g,'').toLowerCase())}`}><Nav.Link onClick={(e) => handleClick(this, idx)} className={'text-decoration-none text-secondary item-text ' + (selected === idx? 'active-item' : 'item')}>{item}</Nav.Link></LinkContainer>
);
})}
</Card>
</>
);
}
export default SettingsChooser;
Firstly, in your parent component when you do
setItems(items)
you are not actually modifying the state, since items already is stored in the state. React will check the value you pass, and not cause a re-render if the value is already stored in the state. When you modify your array with splice, it is still the "same" array, just different contents.
One way around this is to do setItems([...items]), which will call setItems with a new array, containing the same items.
Secondly, in your child class, the following currently has no effect:
useEffect(() => {
setSelected(defaultactive);
}, [])
Since the dependency array is empty, it will only be called on the first render. If you want it to be called any time defaultactive changes, you need to do this instead:
useEffect(() => {
setSelected(defaultactive);
}, [defaultactive])

Fetch firestore document from document id in array field and display in React

I have 2 collections on firestore, boxes contains a field shoesthat is an array of reference id to the shoes collections:
My Boxes component is fetching all boxes and displaying their number. I also want to display properties of the shoes in it, like a photo. So I go about like that:
Boxes.jsx
// DEPENDENCIES
import React, { useEffect, useContext } from 'react';
// COMPONENTS
import BoxCard from '../Components/BoxCard';
// CONTEXT
import ShoesContext from '../Contexts/ShoesContext';
// HELPERS
import db from '../config/firebase';
let initialBoxes = [];
const Boxes = () => {
const { boxes, setBoxes } = useContext(ShoesContext);
useEffect(() => {
initialBoxes = [];
db.collection('boxes')
.get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
initialBoxes.push(doc);
});
setBoxes(initialBoxes);
});
}, []);
return (
<div>
<h3>You have {boxes.length} boxes:</h3>
{!boxes ? (
<div>Loading..</div>
) : (
boxes.map(box => {
return <BoxCard box={box} key={box.id} />;
})
)}
</div>
);
};
export default Boxes;
Boxes.jsx
import React from 'react';
import TestComponent from './TestComponent';
const BoxCard = ({ box }) => {
const theBox = box.data();
return (
<div>
<h5>
Box number {theBox.number} has {theBox.shoes.length} shoes:{' '}
</h5>
{theBox.shoes.map(shoe => {
return <TestComponent shoe={shoe} />;
})}
</div>
);
};
export default BoxCard;
and this is where it all breaks:
import React from 'react';
const TestComponent = ({ shoe }) => {
useEffect(() => {
let hell;
shoe.get().then(doc => {
hell = doc.data();
});
}, []);
return <div>{hell ? hell.season : 'loading...'}</div>;
};
export default TestComponent;
hell is undefined. I have not found a way to render the nested docs so I can use them in my TestComponent component. My extensive research online has not been succesful so far, hence my question today.
Thanks!
Update:
I seem to have found the answer, answer from Josh below put me on the right track. See below code for TestComponent.jsx:
import React, { useEffect, useState } from 'react';
// HELPERS
import db from '../config/firebase';
const TestComponent = ({ shoe }) => {
const [hell, setHell] = useState();
useEffect(() => {
db.collection('shoes')
.doc(shoe.id)
.get()
.then(doc => {
setHell(doc.data());
});
}, []);
return <div>{hell ? hell.season : 'loading...'}</div>;
};
export default TestComponent;
What is shoe in shoe.get()... in the TestComponent? Shouldn't it be db.doc(shoe).get()....

Trying to call useContext outside of a function component

I'm trying to figure out how to correctly use React Context. I'm hung up on this issue of trying to access the Context from outside the function component. I'm getting the error:
Line 9:18: React Hook "useContext" is called in function "onDragEnd" which is neither a React function component or a custom React Hook function react-hooks/rules-of-hooks
Here is my entire Schedule js file:
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { DragDropContext } from 'react-beautiful-dnd';
import OrderColumn from '../ordercolumn/OrderColumn';
import { ScheduleContext } from '../../schedule-context';
const onDragEnd = (result) => {
const { destination, source, draggableId } = result;
const context = useContext(ScheduleContext); // <-- issue is here
if (!destination) {
return;
}
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return;
}
const column = context.columns[source.droppableId];
const orderIDs = Array.from(column.orderIDs);
orderIDs.splice(source.index, 1);
orderIDs.splice(destination.index, 0, draggableId);
const newColumn = {
...column,
orderIDs: orderIDs
};
const newColumns = {
...context.columns,
newColumn
};
context.setColumns(newColumns);
};
const Schedule = () => {
const { orders, setOrders, columns, setColumns } = useContext(
ScheduleContext
);
return (
<DragDropContext onDragEnd={onDragEnd}>
<div className={'full-width'}>
<h1 className={'text-center'}>Schedule</h1>
<div className={'lines row no-gutters'}>
{columns.map(function(val, index) {
if (index === 0) {
return (
<OrderColumn
title={val.title}
columnId={index}
orders={orders}
setOrders={setOrders}
setColumns={setColumns}
/>
);
} else {
return (
<OrderColumn
title={val.title}
columnId={index}
setOrders={setOrders}
setColumns={setColumns}
/>
);
}
})}
</div>
</div>
</DragDropContext>
);
};
Schedule.propTypes = {
orders: PropTypes.array
};
export default Schedule;
Not to sound glib, but essentially it means exactly what it says. onDragEnd is not a React component because it is not returning a ReactElement or some kind of JSX. If you edited your blank return statements to return <div>'s (for all paths) it would be considered a component and work properly, but as of right now it's not returning anything.
Using useCallback and passing the context to that function will help solve your problem.
An example below:
const onDragEnd(result, context) => {
// Adjust as necessary
}
const Schedule = () => {
const context = useContext(ScheduleContext);
const onDragEnd = useCallback((result) => onDragEnd(result, context), [context]);
return <DragDropContext onDragEnd={onDragEnd}>
You can also either inline onDragEnd or pull out more and make a custom hook.

Resources