React child class receiving undefined props even if class is mounted - javascript

I'm trying to send an object as 'props' from a parent Class into a Child class with the intention of showing information:
Parent class
const TaskDetail = () => {
//Get parameter from URL
const { id } = useParams()
const [taskDetail, setTaskDetail] = useState([])
useEffect(() => {
TaskService.getTaskById(id)
.then(response => {
setTaskDetail(response.data);
})
}, [id]) //This [id] avoid infinite loop of requests
if (!taskDetail) return <div>No data</div>
return (
<div>
<Detail taskDetail={taskDetail}/>
</div>
)
}
This class makes a request to the server and gather a data object. This object is then passed onto the Child Detail where it will be deserialized and visualized accordingly.
Child class
const Detail = ({ taskDetail }) => {
return (
<Box
align='center'
justifyContent='center'
sx={{ width: '100%', marginTop: 4}}
bgcolor=''
//border='solid'
>
<Stack
//border='solid'
sx = {{width: '50%'}}
justifyContent='center'
//border='solid'
>
<Typography
sx = {{ marginTop: 5}}
variant='h4'
fontWeight='bold'
bgcolor='#b2dafa'
>NOMBRE DEL EJERCICIO<br/>{taskDetail.taskName}</Typography>
<Box
sx = {{ marginTop: 3}}
bgcolor='#b2dafa'
>
<Typography
variant='h5'
align='center'
sx = {{ margin: 2}}
fontWeight='bold'
>DETALLES DEL EJERCICIO</Typography>
<Typography
sx = {{ margin: 2}}
variant='text'
border='#555'
>{taskDetail.details}
</Typography>
</Box>
<Box
sx = {{ marginTop: 5}}>
<Typography
variant='h6'
>Marca para completar</Typography><Toogle
label=''
toogled={false}
onClick={null}/>
<br></br>
</Box>
{taskDetail.id}
<Box
sx = {{ marginTop: 2}}>
<AddComment taskId={taskDetail.id}/>
</Box>
<Box
sx = {{ marginTop: 2}}>
<ListComments taskId={taskDetail.id}/>
</Box>
</Stack>
</Box>
)
}
As you can observe, this object is also passed to other child components. The context is that TaskDetail shows information and then offers two features, ListComments and AddComments. At the current moment, I am having an issue in AddComment where the prop taskId={taskDetail.id} is undefined.
Function in Child where I am having this issue
function ListComments(props) {
const [comments, setComments] = useState([])
useEffect(() => {
console.log('DEBUG listcomments: ' + props.taskId)
TaskService.getTaskCommentsById(props.taskId)
.then(response => {
setComments(response.data)
})
}, [])
return (
<div>
<h2>Comentarios</h2>
{comments.map((comment, _) => {
return <Comment key={comment.id} comment={comment}/>
})}
</div>
)
}
I have noticed, that if I change something in the code and save it (re-renders the page). All of a sudden I get the atribute that I need instead of the undefined value.
How could I avoid this situation?
I trust that I have made a huge mistake that I am unable to see, but its part of the learning. For this reason, I am open to suggestions.

Since the data you get back from the service is an object I would suggest to initialize the state with an object {}.
const [taskDetail, setTaskDetail] = useState({});
In the ListComments component you can do the same as you did in the TaskDetail component. Run useEffect when the props.taskId changes. And add a early return if the taskId have no value yet.
useEffect(() => {
console.log("DEBUG listcomments: " + props.taskId);
if (!props.taskId) return;
TaskService.getTaskCommentsById(props.taskId).then((response) => {
setComments(response.data);
});
}, [props.taskId]);

Very cool that you're reaching out for help!
First, just a minor correction: They're not classes, they're functions / components.
I think the problem is the check condition at the top:
if (!taskDetail) return <div>No data</div>
Since taskDetail is initialised as an array, the condition will always be true since arrays are objects in javascript.
Because of this too, when you're passing it down, at least on the first render, none of these props in the lower components exist. So maybe try initalising it either as null, or changing the condition to the following:
if (!taskDetail || taskDetail.length === 0) return <div>No data</div>
One more thing, to make sure that the data is fetched, you need to add props.taskId to the dependency list in the List component.

Related

React isn't updating a variable from a function

I'm using axios to return a map to my main app where it will be distributed to other values in the program. I am having an issue though. When I use 'onClick' on a drop down select, I want it to call that external function to return the JSON string and save it to a variable but it won't do it. I have console logged it and it says my variable is use undefined. Here is my axios code
import axios from "axios";
// ** when you launch server. Make sure 'express stuff' server is working and that it is posting to 5000/loadCycle
function Parent() {
let data = null;
console.log("called");
const url = "http://localhost:5000/";
axios
.get(`${url}loadCycle`)
.then((response) => {
data = response.data.workflow;
data = JSON.stringify(data);
data = JSON.parse(data);
//console.log(data);
const map = new Map(Object.entries(data));
console.log(map);
return map;
})
.catch((error) => console.error(`Error: ${error}`));
}
export default Parent;
and here is the code I want to format
function App() {
let dataCollection = null;
return (
<div>
<Box
sx={{ display: "flex", width: "40%", justifyContent: "space-between" }}
>
<Box sx={{ display: "flex" }}>
{/* <Typography sx={{ paddingTop: "6%" }}>Cycle</Typography> */}
{/* Cycle dropdown menu */}
{/* // MAKE CHANGES ON BRANCH // */}
<FormControl
sx={{ m: 1, minWidth: 200 }}
size="small"
variant="standard"
>
<InputLabel>Cycles</InputLabel>
<Select>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10} onClick={dataCollection=Parent()}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
</FormControl>
{/* cycle dropdown menu end */}
</Box>
</div>
)
Why won't selecting 'one' from my select update dataCollection from 'null' to the map I am trying to return to it. Console logging it shows that the 'map' data in Parent is correct but the log for dataCollection is 'undefined'
<MenuItem value={10} onClick={dataCollection=Parent()}>Ten</MenuItem>
First of all you didn't define function (you are tried to do it like in a vainilla js, but react don't work in this way)
So, let's define separate function:
const handleSave = () => {
dataCollection=Parent()
}
// ...
<MenuItem value={10} onClick={handleSave}>Ten</MenuItem>
Next problem that's what your Parent function isn't synchronous, you should return your axios promise and after that use this function like that:
Parent().then(data => {
dataCollection = data;
})
That's not all, we can't save data at dataCollection, because your functional component this is like render function and you will lose your data on next render, so you shoud save your data to ref or state (depending on the purpose of use), let's use state:
const [dataCollection, setDataCollection] = React.useState();
// ...
const handleSave = () => {
Parent().then(data => {
setDataCollection(data);
})
}
Besides this I can see some style issues. And looks like you haven't read react doc attentively, please read againg "state and props" and "lifecycle" subjects from docs.
You have a couple of issues with your approach. I'm not sure what the other components: Box, FormControl, InputLabel, Select, and MenuItem are doing, so it makes it harder to discern if they are functioning correctly. I would simplify the code and just use regular HTML select and option tags. The select tag receives a change event and with React all events can be prefixed with "on", so it would be onChange on the select tag.
Create a prototype, a simpler project, that just focuses on that functionality until you understand it for your needs. Also, practice naming constructs a bit better, as Parent doesn't convey what it is doing. Aim to be succinct and general.

div from method is not rendering on the User Interface in react js

Im using react js and trying to access some dummy data from the holdingsByAccounts prop. The issue jsx is not rendering onto the screen when the page loads. Been looking into other ways and other sites that have some situation as this. So my code is as follows:
class PortDashSideBar extends Component {
constructor(props) {
super(props);
this.oldPortfolioValue = 0;
}
getHoldingsForAccount = (id) => {
this.props.holdingsByAccount.map((holding) => {
return <span>{holding[id].value}</span>;
});
}
renderConfigurations = () => {
const {
classes,
accounts,
holdingsByAccount,
holdingsByAsset,
currentAccountId,
actions,
user
} = this.props;
const animationClassGreen = 'animateChangeGreen';
const animationClassRed = 'animateChangeRed';
return (
<Table className={classes.table} name="myConfigurations">
<TableBody style={{ display: 'flex', flexDirection: 'column' }}>
{
accounts.map(account => (
<ButtonBase>
<TableRow
hover
name={account.name}
key={account.id}
onClick={() => { actions.updateAccount(account.id); }}>
<TableCell className={classes.cellBotIcon} />
<TableCell className={classes.cellName}>
<Typography className={classes.botName}>
{account.label}
</Typography>
<Typography>
<svg className={classes.svg}>
<img src="https://yt3.ggpht.com/ytc/AAUvwngo7rox3GTqcW5Omxr-UGXHwmGO4To3QDygqaYxpg=s900-c-k-c0x00ffffff-no-rj" />
</svg>
<span className={classes.configName}>
{this.getHoldingsForAccount(account.id)}
</span>
</Typography>
</TableCell>
</TableRow>
</ButtonBase>
))
}
</TableBody>
</Table>
);
}
render() {
const {
classes
} = this.props;
return (
<Paper className={classes.paper} elevation={0} square >
<Grid container alignItems="center" justify="center">
<Grid className={classes.configurations} xs={12} item>
{this.renderConfigurations()}
</Grid>
</Grid>
</Paper>
);
}
}
PortDashSideBar.defaultProps = {
};
PortDashSideBar.propTypes = {
actions: PropTypes.object.isRequired,
classes: PropTypes.object.isRequired,
accounts: PropTypes.array.isRequired,
holdingsByAccount: PropTypes.array.isRequired,
holdingsByAsset: PropTypes.array.isRequired,
currentAccountId: PropTypes.string.isRequired,
user: PropTypes.object.isRequired
};
function mapStateToProps(state) {
return {
user: state.global.user.user,
holdingsByAccount: state.holdings.holdings.byAccount,
holdingsByAsset: state.holdings.holdings.byAsset,
holdingsLoaded: state.holdings.holdings.holdingsLoaded,
accounts: state.global.accounts.accounts,
currentAccountId: state.trade.interactions.currentAccountId,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: {
...bindActionCreators({
updateAccount,
}, dispatch)
}
};
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(PortDashSideBar));
Now, it is not showing error, but it is also not rendering on screen
To give you an answer, I think this is because you're trying to access the array item by the index, not by the item id, consider this:
const array = [1,2,3]
console.log(array[3])
// Prints undefined because the index starts in 0, so the index 3 should be the 4th item which does not exist in this array
So your example is trying to access an index that does not exist that is why is crashing. Also is worth to mention that your object id attribute is a string based on the image you upload, so if that string is not a number it will also fail:
const array = [1,2,3]
console.log(array["1"])
// Prints 2 because it parses the string to a number
const array = [1,2,3]
console.log(array["an-id"])
// Prints undefined because the string can't be parsed to a number
So to make things work, change the code to this:
getHoldingsForAccount = (id) => {
// Use Array.find to find the item that matches the id parameter provided by the function
const holding = this.props.holdingsByAccount.find((item) => item.id === id)
// Returns the JSX only if the match was found.
return holding && (
<span>{holding.value}</span>
)
}

How to delete a card from a list of cards (using API to extract Github user information onto a card that is presented in a list)?

This is a react project that uses an API to create a card which presents a Github users information. At the moment I am implementing a delete button on each card that would delete that specific card. You can try the application on code sandbox by clicking here.. Instructions how to use: enter any 'Github' username into the input and click add card.
Once you click on add card, the user information is extracted into a Card which is then stored under CardList. This has an identifiable by key. But when clicking on Delete, instead of deleting the corresponding card to where the delete button is pressed, currently all the cards are deleted. (I have excluded the form component to make it easier to read).
Not sure if I am incorrectly using splice or not correctly declaring the cards key?
function App() {
const [cards, setCards] = useState([]);
const addNewCard = cardInfo => {
setCards(cards.concat(cardInfo));
};
const removeCard = key => {
setCards(cards.splice(key, 0))
}
return (
<div>
<Form onSubmit={addNewCard} />
<CardList
cards={cards}
handleClick={removeCard}
/>
</div>
);
}
const CardList = props => (
<div>
{props.cards.map((card, index) => (
<Card {...card} key={index} handleClick={props.handleClick} />
))}
</div>
);
const Card = props => {
return (
<div style={{ margin: "1em" }}>
<img alt="avatar" style={{ width: "70px" }} src={props.avatar_url} />
<div>
<div style={{ fontWeight: "bold" }}>{props.name}</div>
<div>{props.blog}</div>
<a href={props.html_url}>{props.html_url}</a>
<button onClick={props.handleClick}>Delete</button>
</div>
</div>
);
};
To be exact you incorretly use splice in the context of hooks. Check here to know how splice works
The splice() method adds/removes items to/from an array, and returns the removed item(s).
So you are setting the element you try to remove to your variable. To keep your original logic, I suggest you use a temp variable like this:
const removeCard = key => {
let tempCards = cards;
const removedCard = tempCards.splice(key, 0); // you can use the removed card if needed
setCards(tempCards);
}
And the parameter key is not defined you have to pass this parameter to your function see the documentation :
For just minor change from your code, you can change this:
<Card {...card} key={index} handleClick={props.handleClick} />
to this:
<Card {...card} key={index} handleClick={() => props.handleClick(index)} />
EDIT: When you use concat maybe, it's adding each element of your new card. Try to change this :
setCards(cards.concat(cardInfo));
to this:
setCards(cards.push(cardInfo));
My error here was using splice instead of a slice. Since splice mutates the original array and that should be avoided. But by slicing the array up to the index which you would like to remove, you can then slice from the other side of the index that you would like to delete and concat the second slice to the first.
const removeCard = key => {
setCards(cards.slice(0, key).concat(cards.slice(key + 1, cards.length)))
};
You can find the full code implementation below:
function App() {
const [cards, setCards] = useState([]);
const addNewCard = cardInfo => {
setCards(cards.concat(cardInfo));
};
const removeCard = key => {
setCards(cards.slice(0, key).concat(cards.slice(key + 1, cards.length)))
};
return (
<div>
<Form onSubmit={addNewCard} />
<CardList
cards={cards}
handleClick={removeCard}
/>
</div>
);
}
const CardList = props => (
<div>
{props.cards.map((card, index) => (
<Card {...card} key={index} handleClick={props.handleClick(index)} />
))}
</div>
);
const Card = props => {
return (
<div style={{ margin: "1em" }}>
<img alt="avatar" style={{ width: "70px" }} src={props.avatar_url} />
<div>
<div style={{ fontWeight: "bold" }}>{props.name}</div>
<div>{props.blog}</div>
<a href={props.html_url}>{props.html_url}</a>
<button onClick={props.handleClick}>Delete</button>
</div>
</div>
);
};

Checkboxes in map not updating on array update after refactor to react hooks

I converted a class component into a function component using hooks. Currently, I'm struggling to figure out why the checkboxes within this map is not updating with checked value, despite the onChange handler firing, and updating the array as necessary. (The onSubmit also works, and updates the value within the DB properly).
import {
Container,
Typography,
Grid,
Checkbox,
FormControlLabel,
Button
} from "#material-ui/core";
import Select from "react-select";
import localeSelect from "../services/localeSelect";
import {
linkCharactersToGame,
characterLinked,
linkCharacters
} from "../data/locales";
import dbLocale from "../services/dbLocale";
import { LanguageContext } from "../contexts/LanguageContext";
import { UserContext } from "../contexts/UserContext";
import { GameContext } from "../contexts/GameContext";
import { CharacterContext } from "../contexts/CharacterContext";
import { Redirect } from "react-router-dom";
export default function LinkCharacter() {
const { language } = useContext(LanguageContext);
const { user } = useContext(UserContext);
const { games, loading, error, success, connectCharacters } = useContext(
GameContext
);
const { characters } = useContext(CharacterContext);
const [game, setGame] = useState("");
const [selectedCharacters, setSelectedCharacters] = useState([]);
if (!user) {
return <Redirect to="/" />;
}
return (
<section className="link-character">
<Container maxWidth="sm">
<Typography variant="h5">
{localeSelect(language, linkCharactersToGame)}
</Typography>
{error && (
<p className="error">
<span>Error:</span> {error}
</p>
)}
{success && <p>{localeSelect(language, characterLinked)}</p>}
<Select
options={games.map(game => {
return {
label: dbLocale(language, game),
value: game._id
};
})}
onChange={e => {
setGame(e.value);
const selected = [];
const index = games.findIndex(x => x._id === e.value);
games[index].characters.forEach(character => {
selected.push(character._id);
});
setSelectedCharacters(selected);
}}
/>
</Container>
<Container maxWidth="md">
{game !== "" && (
<>
<Grid container spacing={2}>
{characters.map((character, index) => {
return (
<Grid item key={index} md={3} sm={4} xs={6}>
<FormControlLabel
control={
<Checkbox
value={character._id}
onChange={e => {
const index = selectedCharacters.indexOf(
e.target.value
);
if (index === -1) {
selectedCharacters.push(e.target.value);
} else {
selectedCharacters.splice(index, 1);
}
}}
color="primary"
checked={
selectedCharacters.indexOf(character._id) !== -1
}
/>
}
label={dbLocale(language, character)}
/>
</Grid>
);
})}
</Grid>
<Button
variant="contained"
color="primary"
onClick={e => {
e.preventDefault();
connectCharacters(game, selectedCharacters);
}}
>
{localeSelect(language, linkCharacters)}
</Button>
</>
)}
</Container>
</section>
);
}
I feel like there's something I'm missing within Hooks (or there's some sort of issue with Hooks handling something like this). I have been searching and asking around and no one else has been able to figure out this issue as well.
The state returned by [state, setState] = useState([]) is something that you should only be reading from. If you modify it, React won't know that the data has changed and that it needs to re-render. When you need to modify data, you have to use setState, or in your case setSelectedCharacters.
Also, modifying the data by reference might lead to unpredictable results if the array is read elsewhere, later on.
In addition to that, if you give the same value to setState, that the hook returned you in state, React will skip the update entirely. It is not a problem when using numbers or strings, but it becomes one when you use arrays, because the reference (the value React uses to tell if there is a difference) can be the same, when the content might have changed. So you must pass a new array to setState.
With that in mind, your onChange function could look like:
onChange={e => {
const index = selectedCharacters.indexOf(
e.target.value
);
if (index === -1) {
// creating a new array with [], so the original one stays intact
setSelectedCharacters([...selectedCharacters, e.target.value]);
} else {
// Array.filter also creates new array
setSelectedCharacters(selectedCharacters.filter((char, i) => i !== index));
}
}}
Doc is here https://en.reactjs.org/docs/hooks-reference.html#usestate

How to access JSON object in React state?

setRadio= (id) => {
const {formRating} = this.state;
fetch(`http://localhost:3030/getLessonCondsDB?formId=${id}`)
.then(response => response.json())
.then(response=>{
this.setState({formRating:response.data})
console.log(response.data);})
.catch(err=>console.error(err))
}
The above method assigns the JSON object which is displayed in console as [RowDataPacket {condId: 'C2.1(a)', rate: 3, condition: 'Random text here' }, RowDataPacket {condId: 'C2.2(b)',rate: 3,condition: 'more random text' }]to the state object formRating which is displayed in dev tools as below
formRating: Array
> 0: Object
condId: 'C2.1(a)'
rate: '3',
condition: 'Random text here'
> 1: Object
condId: 'C2.2(b)'
rate: '3',
condition: 'more random text'
Any attempt to console.log(formRating) just prints and empty line on the console.
Instead of fetching from the server I had previously hardcoded this data into an array as below
const formValues= [{condId :'C2.1(a)',rate:'3', condition:'Random text here'},{condId :'C2.2(b)',rate:'3', condition:'more random text'}]
and had a method in another component to create radioGroups mapping each set of conditions allowing users to change the rate value as discussed in How to set defaultValue of a radioGroup from a nested Array object in React state? which works with the hardcoded array but not the JSON array which produces a "TypeError: values.formRating.map is not a function" with the below function in the component where radioGroups are displayed allowing the user to customise the "rate" value.
createRadioGroups = ()=>{
const {values} = this.props;
console.log(values.formRating);
return(values.formRating.map(
item =>
<Grid container>
<Grid item xs={2} style={{marginTop:20, marginRight:0}}>{item.condId} </Grid>
<Grid item xs={6} style={{marginTop:20}}>{item.condition} </Grid>
<Grid item xs={4} style={{marginTop:10}}>
<RadioGroup defaultValue={item.rate} name={item.condId} onChange={this.changeButton(item.condId)} style={{display: 'flex', flexDirection: 'row'}}>
<FormControlLabel value="3" control={<Radio color="primary" />} label=' ' labelPlacement="top"/>
<FormControlLabel value="2" control={<Radio color="primary" />}label=' ' labelPlacement="top"/>
<FormControlLabel value="1" control={<Radio color="primary" />}label=' ' labelPlacement="top"/>
<FormControlLabel value="N/A" control={<Radio color="primary" />}label=' ' labelPlacement="top"/>
</RadioGroup>
</Grid>
</Grid>
))
};
Any help is appreciated.
That is because the fetch operation within setRadio() is asynchronous, thus any operations that are dependent on the state, or the values from setRadio() will fail. This is why calling createRadioGroups() before setRadio() is returned and completed will result in an undefined value.
I am not sure how exactly is your component structured, but you should handle any subsequent operations within the .then() block,
setRadio= (id) => {
const {formRating} = this.state;
fetch(`http://localhost:3030/getLessonCondsDB?formId=${id}`)
.then(response => response.json())
.then(response=>{
this.setState({formRating:response.data})
console.log(response.data);
// do the rest here
})
.catch(err=>console.error(err))
}
Or if the rendering is handled on the template, you should conditionally call the method only after formRating is populated.
render() {
const { formRating } = this.state;
return <>
{ formRating && formRating.length && this.createRadioGroups() }
</>
}
Or, if createRadioGroups() is on another child component,
render() {
const { values } = this.props;
return <>
{ values && values.formRating && values.formRating.length && this.createRadioGroups() }
</>
}
How are you passing the 'values' prop to the createRadioGroup? Seems like you need to pass it in (see snippet below) then try console logging the entire props object to make sure you are actually receiving it.
createRadioGroups = (props)=>{
const {values} = this.props;
After you check that, then consider when you are calling setRadio? Are you sure the state has already been updated so that it is available when you call createRadioGroup? If it possibly hasn't been updated then you can try initializing your state with the 'shape' of your expected data so that it will render with no data, then rerender once the state is updated. Would look something like this:
this.state = {
formValues=
[
{
condId :'',
rate:'',
condition:''
}
];
Try this
return(
<>
this.props.values&&this.props.values.formRating.map()
</>
)

Categories