Why is my functional component's state undefined? - javascript

I'm working on a flashcard App, my flashcard component is as below:
import React, { useState } from 'react';
import './Flashcard.css'
import {
Button,
} from '#material-ui/core';
function Flashcard({ data }) {
// // Get an initial card
const [currentCard, setCurrentCard ] = useState(data[0]);
// Create state to track whether the card has been flipped yet
const [flipped, setFlipped] = useState(false);
// When the Flip button is clicked, we display the verso
const handleFlipClick = () => {
setFlipped(true);
}
const handleNextClick = () => {
// Change the current active card
setCurrentCard(data[1]);
// Restore the 'flipped' state to indicate that the new active card hasn't been flipped yet
setFlipped(false);
}
// Get data needed from the server
return (
<div className="flashcard__container">
{/* Card content goes here */}
<div className="flashcard__content">
<p>
{ (flipped) ? currentCard.verso : currentCard.recto }
</p>
</div>
<div className="flashcard__actions">
{/* Display the flip button if the card hasn't been flipped yet */}
{(!flipped) ?
<Button
variant="contained"
color="primary"
onClick={handleFlipClick}
>Flip</Button>
:
<>
<Button onClick={handleNextClick} color='primary' variant='contained'>
Hard
</Button>
<Button onClick={handleNextClick} color='primary' variant='contained'>
Slightly difficult
</Button>
<Button onClick={handleNextClick} color='primary' variant='contained'>
Good
</Button>
<Button onClick={handleNextClick} color='primary' variant='contained'>
Easy
</Button>
</>
}
</div>
</div>
)
}
export default Flashcard
'data' is pass from another component, when I console log it in the Flashcard component, here is what I get:
[
0: {id: "PynaJl43Jphl2xI8fWkn", reviewedOn: Array(2), recto: "To go", verso: "行く"}
1: {id: "mSDdg5ATenvjBYojfy8N", verso: "こんにちは", recto: "Hi", reviewedOn: Array(2)
]
However, this line: (flipped) ? currentCard.verso : currentCard.recto gives me the following error message: "Type Error - Cannot read property 'recto' of undefined".
Indeed, when I try to console log 'currentCard', it says it is undefined. Why is that happening? I don't see any mistake in the way I initialized the state. Thank you!
Here is the parent component:
import React, { useState, useEffect } from 'react';
import FlashCard from './Flashcard';
import mockData from './mockData';
import { getParams, useParams } from 'react-router-dom';
import { db } from './firebase';
import Header from './Header';
function Deck() {
const { id } = useParams();
const [cards, setCards] = useState(); // Create a state to store the deck
// When the page is first loaded: Import the deck's card from the database
useEffect(() => {
db.collection(`decks/${id}/cards`)
// Return an array of cards
.onSnapshot((snapshot) => {
setCards(snapshot.docs.map((doc) => {
return {
id: doc.id,
...doc.data()
}
}))
})
}, [])
return (
<div>
<Header />
{cards && <FlashCard data={cards} />}
</div>
)
}
export default Deck

data is likely undefined when this component is first rendered. You can fix this in one of two ways.
1.) Add logic to the parent component so that Flashcard is not rendered until data is defined.
{data && <Flashcard data={data} />}
2.) Add a useEffect that monitors for changes to data and updates state.
React.useEffect(() => {
if(typeof currentCard === 'undefined'){
setCurrentCard(data[0])
}
}, [data])

Maybe you are passing some faulty data into the props. Try my sample, it works:
import React, { useState } from "react";
import "./styles.css";
// import { FlashCard } from "./FlashCard";
import { Button } from "#material-ui/core";
function Flashcard({ data }) {
// // Get an initial card
// // Get an initial card
const [currentCard, setCurrentCard] = useState(data[0]);
// Create state to track whether the card has been flipped yet
const [flipped, setFlipped] = useState(false);
// When the Flip button is clicked, we display the verso
const handleFlipClick = () => {
setFlipped(true);
};
const handleNextClick = () => {
// Change the current active card
setCurrentCard(data[1]);
// Restore the 'flipped' state to indicate that the new active card hasn't been flipped yet
setFlipped(false);
};
// Get data needed from the server
return (
<div className="flashcard__container">
{/* Card content goes here */}
<div className="flashcard__content">
<p>{flipped ? currentCard.verso : currentCard.recto}</p>
</div>
<div className="flashcard__actions">
{/* Display the flip button if the card hasn't been flipped yet */}
{!flipped ? (
<Button variant="contained" color="primary" onClick={handleFlipClick}>
Flip
</Button>
) : (
<>
<Button
onClick={handleNextClick}
color="primary"
variant="contained"
>
Hard
</Button>
<Button
onClick={handleNextClick}
color="primary"
variant="contained"
>
Slightly difficult
</Button>
<Button
onClick={handleNextClick}
color="primary"
variant="contained"
>
Good
</Button>
<Button
onClick={handleNextClick}
color="primary"
variant="contained"
>
Easy
</Button>
</>
)}
</div>
</div>
);
}
export default function App() {
return (
<div className="App">
<Flashcard
data={[
{
id: "PynaJl43Jphl2xI8fWkn",
reviewedOn: Array(2),
recto: "To go",
verso: "行く"
},
{
id: "mSDdg5ATenvjBYojfy8N",
verso: "こんにちは",
recto: "Hi",
reviewedOn: Array(2)
}
]}
></Flashcard>
</div>
);
}

I recreated this in a CodeSandbox and it worked just fine. My guess is you have an error on the props name when passing props to the Flashcard component?
It should be rendered as seen below since in Flashcard you are destructuring data from props:
<Flashcard data = {insertArrayHere} />

I would suggest you using useEffect and adding data as a dependency. Then in useEffect check, if the data exists it should change the state. You can make it this way:
useEffect(() => {
if(data){
setCurrentCard(data[0]);
}
}, [data])
or you can check existing of this prop in the parent element, adn if it exists show it that time. You can implement it in this way:
{ data ? <Flashcard data={data} /> : null }

Related

How to use state value in another js file

I have a file named Card.js. It contains two states called status and view .
Every time a button is clicked, my status state changes & according to this state my card is hidden.
At the same time, each of my cards has a view state. This increases by 1 according to the click event.
src/components/Card.js
const Card = (props) => {
const [status, setStatus] = useState(false);
const [view, setView] = useState(0);
const handleClick = () => {
setStatus(!status);
setView(view + 1);
};
return (
<div>
<Button onClick={handleClick}>{status ? "Show Component" : "Hide Component"}</Button>
{status ? null : <div className="card">{props.children} </div>}
</div>
);
};
export default Card;
In my app.js file, I return the data in my JSON file with the map method, I print this data to my card component.
src/App.js
const App= () => {
return (
<div>
{post.map((value, index) => {
return (
<Card key={index}>
{// here I want to print the "view" state in my Card.js file.}
</Card>
);
})}
</div>
);
};
export default App;
In App.js, I tried to create the view state using the useEffect hook, but every time I click the button, the view state of both my cards is updated at the same time. I don't want it to happen this way.
You can pass the data in your card as props so that the data is available in your Card module.
{
post.map((value, index) => {
return (
<Card key={index} status={false}>
{// here I want to print the "view" state in my Card.js file.}
</Card>
);
})
}
And in card module, pass the prop value to useState. I hope this will solve your problem.
const [status, setStatus] = useState(props.status);

React render list only when data source changes

Basically I have a modal with a state in the parent component and I have a component that renders a list. When I open the modal, I dont want the list to re render every time because there can be hundreds of items in the list its too expensive. I only want the list to render when the dataSource prop changes.
I also want to try to avoid using useMemo if possible. Im thinking maybe move the modal to a different container, im not sure.
If someone can please help it would be much appreciated. Here is the link to sandbox: https://codesandbox.io/s/rerender-reactmemo-rz6ss?file=/src/App.js
Since you said you want to avoid React.memo, I think the best approach would be to move the <Modal /> component to another "module"
export default function App() {
return (
<>
<Another list={list} />
<List dataSource={list} />
</>
);
}
And inside <Another /> component you would have you <Modal />:
import React, { useState } from "react";
import { Modal } from "antd";
const Another = ({ list }) => {
const [showModal, setShowModal] = useState(false);
return (
<div>
<Modal
visible={showModal}
onCancel={() => setShowModal(false)}
onOk={() => {
list.push({ name: "drink" });
setShowModal(false);
}}
/>
<button onClick={() => setShowModal(true)}>Show Modal</button>
</div>
)
}
export default Another
Now the list don't rerender when you open the Modal
You can use React.memo, for more information about it please check reactmemo
const List = React.memo(({ dataSource, loading }) => {
console.log("render list");
return (
<div>
{dataSource.map((i) => {
return <div>{i.name}</div>;
})}
</div>
);
});
sandbox here

React and PropTypes

Some time ago I tried to make my first steps in React. Now I'm trying to create simple ToDo list app. One of my colleagues told me that it will be nice to use PropTypes with shape and arrayOf. I have one component which pass data to another and here is my issue(List.js and ListItem.js). I'm not sure when, where and how to use propTypes with shape and arrayOf. Which approach is right? Define PropTypes in parent component or in child component? Maybe there is a better idea? Please find my code below, it will be nice if you can clarify me how to use PropTypes. I tried to search something in Web, but I still don't get it.
Main App.js file:
import React from "react";
import "./styles/App.scss";
import List from "./components/List/List";
import AppHeader from "./components/AppHeader/AppHeader";
function App() {
return (
<div className="App">
<AppHeader content="TODO List"></AppHeader>
<List></List>
</div>
);
}
export default App;
AppHeader.js file:
import React from "react";
import "./AppHeader.scss";
export default function AppHeader(props) {
return (
<div className="header">
<h1 className="header-headline">{props.content}</h1>
</div>
);
}
List.js file:
import React from "react";
import "./List.scss";
import ListItem from "../ListItem/ListItem";
import _ from "lodash";
const initialList = [
{ id: "a", name: "Water plants", status: "done" },
{ id: "b", name: "Buy something to eat", status: "in-progress" },
{ id: "c", name: "Book flight", status: "in-preparation" }
];
const inputId = _.uniqueId("form-input-");
// function component
const List = () => {
const [value, setValue] = React.useState(""); // Hook
const [list, setList] = React.useState(initialList); // Hook
const handleChange = event => {
setValue(event.target.value);
};
// add element to the list
const handleSubmit = event => {
// prevent to add empty list elements
if (value) {
setList(
list.concat({ id: Date.now(), name: value, status: "in-preparation" })
);
}
// clear input value after added new element to the list
setValue("");
event.preventDefault();
};
// remove current element from the list
const removeListItem = id => {
setList(list.filter(item => item.id !== id));
};
// adding status to current element of the list
const setListItemStatus = (id, status) => {
setList(
list.map(item => (item.id === id ? { ...item, status: status } : item))
);
};
return (
<div className="to-do-list-wrapper">
<form className="to-do-form" onSubmit={handleSubmit}>
<label htmlFor={inputId} className="to-do-form-label">
Type item name:
</label>
<input
type="text"
value={value}
onChange={handleChange}
className="to-do-form-input"
id={inputId}
/>
<button type="submit" className="button to-do-form-button">
Add Item
</button>
</form>
<ul className="to-do-list">
{list.map(item => (
<ListItem
name={item.name}
status={item.status}
key={item.id}
id={item.id}
setListItemStatus={setListItemStatus}
removeListItem={removeListItem}
></ListItem>
))}
</ul>
</div>
);
};
export default List;
And ListItem.js file:
import React from "react";
import PropTypes from "prop-types";
import "./ListItem.scss";
const ListItem = props => {
return (
<li className={"list-item " + props.status} key={props.id}>
<span className="list-item-icon"></span>
<div className="list-item-content-wrapper">
<span className="list-item-text">{props.name}</span>
<div className="list-item-button-wrapper">
<button
type="button"
onClick={() => props.setListItemStatus(props.id, "in-preparation")}
className="button list-item-button"
>
In preparation
</button>
<button
type="button"
onClick={() => props.setListItemStatus(props.id, "in-progress")}
className="button list-item-button"
>
In progress
</button>
<button
type="button"
onClick={() => props.setListItemStatus(props.id, "done")}
className="button list-item-button"
>
Done
</button>
<button
type="button"
onClick={() => props.removeListItem(props.id)}
className="button list-item-button"
>
Remove
</button>
</div>
</div>
</li>
);
};
ListItem.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
name: PropTypes.string,
status: PropTypes.string,
removeListItem: PropTypes.func,
setListItemStatus: PropTypes.func
};
export default ListItem;
Props are per component.
The purpose of prop types is to let you make some type checking on a component's props.
It seems like you did it correctly in the ListItem component.
Basically, you just list all of the component's props and what type they should be.
Like you did here:
ListItem.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
name: PropTypes.string,
status: PropTypes.string,
removeListItem: PropTypes.func,
setListItemStatus: PropTypes.func
};
I am not exactly sure where will the propTypes shape and arrayOf be used, but generally, PropTypes are useful when you are trying to render a child component within a parent component, and you want to carry out TypeChecking on the props of the relative child component.
In your scenario, arrayOf and shape can be used in one of the props within your component whereby the prop is an array of a certain type (arrayOf), and an object of a certain type(shape, or exact).

data does not re-render after clicking the sort button

I have milestoneCards.
I want to add a sort button, that upon clicking this button the cards will be sorted by the card heading.
The sort takes place, but it does not re-render the list in the sorted order.
please advise.
thank you so much for helping me here.
import React from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { Card, CardBody, CardTitle } from "reactstrap";
const MyMilestones = props => {
let sortClicked = false;
let milestoneCards =
props.milestones.length > 0
? props.milestones.map(m => (
<p key={m.id}>
<Link to={`/milestones/${m.id}`}>{m.attributes.heading}</Link>
</p>
))
: null;
const sortedMilestoneCards = [...props.milestones]
.sort((a, b) => (a.attributes.heading > b.attributes.heading ? 1 : -1))
.map(m => (
<p key={m.id}>
<Link to={`/milestones/${m.id}`}>{m.attributes.heading}</Link>
</p>
));
return (
<div className="MilestoneCards">
{
<Card>
<CardBody>
<CardTitle>
<h4>My Milestones</h4>
</CardTitle>
<button
onClick={() => {
sortClicked = true;
console.log("before", milestoneCards);
milestoneCards = sortedMilestoneCards;
console.log("after", milestoneCards);
return (milestoneCards = sortedMilestoneCards);
}}
>
Sort
</button>
sortClicked ? ({sortedMilestoneCards}) : {milestoneCards}
</CardBody>
</Card>
}
</div>
);
};
const mapStateToProps = state => {
return {
milestones: state.myMilestones
};
};
export default connect(mapStateToProps)(MyMilestones);
It's because you need to have sortClicked to be tracked by React.
When let sortClicked = false is declared inside MyMilestones component, it's declared once on the first component mount and won't be updated when the component is re-rendered.
So you can save sortClicked in a state using React.useState and update it onClick. useState is a one-off way of storing this.state value for Class Component but for one state. (I won't get into it too deep as React documentation has a thorough coverage on Introducing Hooks)
const MyMilestones = props => {
// let sortClicked = false;
// Initialize it to "false" by default.
let [sortClicked, setSortClicked] = React.useState(false)
let milestoneCards = ...;
const sortedMilestoneCards = ...;
return (
<div className="MilestoneCards">
{
<Card>
<CardBody>
<CardTitle>
<h4>My Milestones</h4>
</CardTitle>
<button
onClick={() => {
// Notify "React" to re-render.
setSortClicked(true)
// No need to return a new reference here.
}}
>
Sort
</button>
{/* 👇 Note that {} is wrapped around the whole block. */}
{sortClicked ? sortedMilestoneCards : milestoneCards}
</CardBody>
</Card>
}
</div>
);
};
It's because you're not updating the milestones correctly. Since they're stored on Redux state, you need to add and dispatch the action that modifies the state.
I recommend you look at the Redux documentation.

Function not working on onClick event in React.js

I am writing a react program where data will be fetched through an API and will be displayed in a table. There are 3 main files App.js, Table.js and Search.js and Button.js.
The data is being displayed, the search is working but delete button is not working.
I have written a function for delete button and I guess something is wrong in that but don't know what.
App.js
import React, { Component } from 'react';
import './App.css';
import Table from './components/Table';
import Search from './components/Search';
//API config
const DEFAULT_QUERY = 'react';
const PATH_BASE = 'https://hn.algolia.com/api/v1';
const PATH_SEARCH = '/search';
const PARAM_SEARCH = 'query=';
const url = `${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${DEFAULT_QUERY}`;
class App extends Component {
constructor(){
super();
//here searchText is set to DEFAULT_QUERY which will return the result for keyword "redux"
//refer line 8 to change
this.state={
searchText:'',
result:''
}
this.onDismiss=this.onDismiss.bind(this);
this.onSearchChange=this.onSearchChange.bind(this);
this.searchStories=this.searchStories.bind(this);
//this.isSearched=this.isSearched.bind(this);
}
//to add a delete button
onDismiss=(id)=>{
//filter out item array and return results with no matched id
const deleteList=this.state.list.filter(item=>item.objectID!==id);
//setting state of list to lastest deleteList
this.setState({
result:deleteList
})
}
//to add a search bar
onSearchChange=(e)=>{
//set state to value in search bar
this.setState({
[e.target.name]:e.target.value
})
}
searchStories=(result)=>{
this.setState({
result
});
}
//after mounting will fetch the api data
componentDidMount(){
fetch(url)
.then(response => response.json())
.then(result => this.searchStories(result));
}
render() {
const {result,searchText}=this.state;
if(!result){
return null;
}
return(
<div className="page">
<div className="interactions">
<Search
searchText={searchText}
onSearchChange={this.onSearchChange}
>
Search
</Search>
</div>
<Table
list={result.hits}
onDismiss={this.onDismiss}
searchText={searchText}/>
</div>
)
}
}
export default App;
Table.js
import React from 'react';
import Button from './Button';
const Table=(props)=>{
const {list,searchText,onDismiss}=props;
return(
<div className="table">
{/*Filter out item title and search title and give away results from item array */}
{list.filter((item)=>{
{/*The includes() method determines whether an array includes a certain value among its entries
, returning true or false as appropriate. */}
return item.title.toLowerCase().includes(searchText.toLowerCase());}).map((item)=>{
return(
<div key={item.objectID} className="table-row">
<span style={{ width: '40%' }}>
<a href={item.url}>{item.title}</a>
</span>
<span style={{ width: '30%' }}>
{item.author}
</span>
<span style={{ width: '10%' }}>
{item.num_comments} comments
</span>
<span style={{ width: '10%' }}>
${item.points} points
</span>
<span style={{ width: '10%' }}>
<Button className="button-inline" onClick={()=>onDismiss(item.objectID)}>delete</Button>
</span>
</div>
)})}
</div>
)
}
export default Table;
Button.js
import React from 'react';
const Button=(props)=>{
const{onclick,className='',children}=props;
return(
<div>
<button onClick={onclick} className={className} >{children}</button>
</div>
)
}
export default Button;
Your button needs to be modified slightly:
<button onClick={onClick} className={className} >{children}</button>
The handler needs to refer to the props passed in which are this.props.onClick, not this.props.onclick (which you had).
The error you are encountering can be fixed by modifying the App.js:
onDismiss = id => {
if (id) {
const deleteList = this.state.list.filter(item => item.objectID !== id);
// setting state of list to lastest deleteList
this.setState({
result:deleteList
})
}
}
In Button component change
const{onclick,className='',children}=props;
to
const{onClick,className='',children}=props;
Also it seems that you have not set list in the state therefore when you try to access this.state.list.filter it will throw an error.
onDismiss=(id)=>{
const deleteList=this.state.result.hits.filter(item=>item.objectID!==id);
this.setState({
result:{...this.state.result,hits:deleteList}
})
}

Categories

Resources