how to change state of a react component from another component - javascript

I saw dozens of examples but they are not working in my case, I want to update the page variable in "bookitem" component and rerender it. using gives an error ' Expected an assignment or function call and instead saw an expression no-unused-expressions'
import React from 'react'
import { Pagination, Container } from 'semantic-ui-react'
import bookitem from './Book_item'
const PaginationI = () => (
<Container style={{textAlign: "center", padding:'4rem'}}>
<Pagination defaultActivePage={5} totalPages={10} onPageChange={PageChange}/>
</Container>
)
function PageChange(event,data){
console.log(data.activePage);
<bookitem page={data.activePage}/>
};
export default PaginationI
//////////////////////////////////////////////////////////////////////////////////////////////////////
class bookitem extends Component{
constructor(props){
super (props);
this.state={
counter:0,
page:0,
data2:[]
};
}
componentWillMount(){
console.log(this.props.page)
axios.get('/books/'+this.state.page).then(res=>{console.log(res.data);this.setState({data2:res.data});})
console.log('aa')
console.log(this.state.data2)
}
genurl(isbn){
console.log(isbn)
let url='http://covers.openlibrary.org/b/isbn/'+ isbn + '-L.jpg'
return url;
}
render(){return(
<div>
<div>{this.state.page}</div>
<Container>
<div style={{padding:"1em 1em", textAlign: "right"}}>
<Card.Group itemsPerRow={3} stackable={true} doubling={true}>
{this.state.data2.map(card=>(
<Card href="#">
<Image src={this.genurl(card.isbn)} wrapped ui={false} />
<Card.Content>
<Card.Header>{card.title}</Card.Header>
<Card.Meta>
<span className='date'>Author:{card.author}</span>
</Card.Meta>
<Card.Content >
<Rating icon='star' defaultRating={card.avgrating} maxRating={5} />
</Card.Content>
<Card.Description>
{card.avgrating} Avg rating, {card.totalratings} total ratings.
</Card.Description>
</Card.Content>
<Card.Content >
<a>
<Icon name='pencil alternate' />
{card.reviews} Reviews
</a>
</Card.Content>
</Card>
))}
</Card.Group>
</div>
</Container>
</div>
)
}
}
export default bookitem

The problem is that you are not rendering the bookitem component at all. You have to manage the state of your activePage, pass it to the bookitem and actually render this component.
import React, { useState } from "react";
import { Pagination, Container } from "semantic-ui-react";
import BookItem from "./Book_item";
const PaginationI = () => {
const [activePage, setActivePage] = useState(0); // manage the state of activePage
function PageChange(event, data) {
setActivePage(data.activePage); // update the state in event handler
}
return (
<Container style={{ textAlign: "center", padding: "4rem" }}>
<BookItem page={activePage} /> {/* render your component */}
<Pagination
defaultActivePage={5}
totalPages={10}
onPageChange={PageChange} /> {/* pass event handler */}
</Container>
);
};
export default PaginationI;
Also you would have to rename the bookitem component due to collision with HTML tags like this
import React from "react";
class BookItem extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
page: 0,
data2: [],
};
}
componentWillMount() {
console.log(this.props.page);
axios.get("/books/" + this.state.page).then((res) => {
console.log(res.data);
this.setState({ data2: res.data });
});
console.log("aa");
console.log(this.state.data2);
}
genurl(isbn) {
console.log(isbn);
let url = "http://covers.openlibrary.org/b/isbn/" + isbn + "-L.jpg";
return url;
}
render() {
return (
<div>
<div>{this.state.page}</div>
<Container>
<div style={{ padding: "1em 1em", textAlign: "right" }}>
<Card.Group itemsPerRow={3} stackable={true} doubling={true}>
{this.state.data2.map((card) => (
<Card href="#">
<Image src={this.genurl(card.isbn)} wrapped ui={false} />
<Card.Content>
<Card.Header>{card.title}</Card.Header>
<Card.Meta>
<span className="date">Author:{card.author}</span>
</Card.Meta>
<Card.Content>
<Rating
icon="star"
defaultRating={card.avgrating}
maxRating={5}
/>
</Card.Content>
<Card.Description>
{card.avgrating} Avg rating, {card.totalratings} total
ratings.
</Card.Description>
</Card.Content>
<Card.Content>
<a>
<Icon name="pencil alternate" />
{card.reviews} Reviews
</a>
</Card.Content>
</Card>
))}
</Card.Group>
</div>
</Container>
</div>
);
}
}
export default BookItem;

First of all Bookitem must starts with capitalized letter. So instead of <bookitem /> you must have <Bookitem/>.
Now if you want to change state of a react component from another component, you have to pass a function from parent to child which will be called when you want to change the state. For example
const Compoent1 = () => {
const [state, setState] = useState(value)
.....
return <Component2 changeState={setState} />
}

Related

Material-ui Tab clicking submit to go to the next page removes the tab. How can I fix this?

I have these tabs to go to the next step. However, once clicking submits, this will go to the correct page, but this will also remove the tab. How can I fix this?
I have recreated this in codesandbox: https://codesandbox.io/s/dawn-cloud-uxfe97?file=/src/App.js
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Typography component="span">{children}</Typography>
</Box>
)}
</div>
);
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired
};
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`
};
}
const Ordering = () => {
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<div>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={value}
onChange={handleChange}
aria-label="basic tabs example"
>
<Tab
label="Step 1"
{...a11yProps(0)}
component={Link}
to={`/step1`}
/>
<Tab label="Step 2" {...a11yProps(1)} />
</Tabs>
</Box>
<TabPanel value={value} index={0}>
<Step1 />
</TabPanel>
<TabPanel value={value} index={1}>
<Step2 />
</TabPanel>
</div>
);
};
export default Ordering;
Step1.js
Navigating this to the step2 component does go to the next page, but this will also remove the tab
import { useNavigate } from "react-router-dom";
const Step1 = () => {
const navigate = useNavigate();
const handleSubmit = (e) => {
e.preventDefault();
navigate("/Step2");
};
return (
<div>
<form onSubmit={handleSubmit}>
<input type="submit" />
</form>
</div>
);
};
export default Step1;
Step2.js
const Step2 = () => {
return <div>Step2</div>;
};
export default Step2;
In your case you're not nesting your routes properly. So, For nested route you need to define a route inside another one. Refer this page https://reactrouter.com/docs/en/v6/api#routes-and-route for more information.
Getting back to your question. You need to update your code in two place. First how the routes are defined.
<Route path="/page" element={<Page />}>
<Route path="step1" element={<Step1 />} />
<Route path="step2" element={<Step2 />} />
</Route>
Once, your routes are updated. You are linking your panels based on route. So, you need not define them again in your Page component. You can remove those component from there and just add code so that when tab is click you change your navigation bar url. Sadly Tab of mui doesn't support component property https://mui.com/api/tab/ . You have to do that manually. You can use useNavigate provided by react router dom. Your updated Page component would look like this
I have added comment // This is added. To see where I've made changes. Just in 2 places changes are required.
import React, { useState, useEffect } from "react";
import { Box, Tab, Typography, Tabs } from "#mui/material";
import PropTypes from "prop-types";
import Step1 from "./Step1";
import Step2 from "./Step2";
import { Link } from "react";
// hook is imported
import { useNavigate } from "react-router-dom";
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Typography component="span">{children}</Typography>
</Box>
)}
</div>
);
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired
};
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`
};
}
const paths = ['/page/step1', '/page/step2']
const Ordering = () => {
const [value, setValue] = React.useState(0);
//This is added
const navigate = useNavigate()
const handleChange = (event, newValue) => {
setValue(newValue);
// This is added
navigate(paths[newValue])
};
return (
<div>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={value}
onChange={handleChange}
aria-label="basic tabs example"
>
<Tab
label="Step 1"
{...a11yProps(0)}
component={Link}
to={`/step1`}
/>
<Tab label="Step 2" {...a11yProps(1)} />
</Tabs>
</Box>
{/* Removed tab panels from here */}
</div>
);
};
export default Ordering;

pass a data from a react component to another component which are on different routes

I have a CountryList react component
import React from "react";
import { Link } from "react-router-dom";
import { BsSearch } from "react-icons/bs";
export default function CountryList({
countries,
}: {
countries: any;
}): JSX.Element {
const [filter, setFilter] = React.useState("");
const [sortType, setSortType] = React.useState("");
console.log(filter);
const sorted = countries.sort((a: { name: string }, b: { name: any }) => {
const isReversed = sortType === "asc" ? 1 : -1;
return isReversed * a.name.localeCompare(b.name);
});
const onSort = (sortType: React.SetStateAction<string>) => {
console.log("changed");
setSortType(sortType);
};
return (
<div style={{ marginTop: "3rem" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "10px",
}}
>
<div>List of countries</div>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ position: "relative", marginRight: "1rem" }}>
<input
type="text"
placeholder="Filter"
name="namePrefix"
style={{ padding: "0.35rem" }}
onChange={(e: any) => {
setFilter(e.target.value);
}}
/>
<div style={{ position: "absolute", top: "5px", right: "5px" }}>
<BsSearch size="16" />
</div>
</div>
<div style={{ width: "8rem" }}>
<div className="btn-group">
<button
type="button"
className="btn dropdown-toggle sort-button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{sortType === "asc"
? "Ascending"
: sortType === "desc"
? "Descending"
: "Select"}
</button>
<ul className="dropdown-menu sort-button">
<li>
<button
className="dropdown-item"
type="button"
onClick={() => onSort("asc")}
>
Ascending
</button>
</li>
<li>
<button
className="dropdown-item"
type="button"
onClick={() => onSort("desc")}
>
Descending
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
<div className="country-list-items">
{countries &&
sorted.map((item: any, index: number) => (
<div key={index}>
<Link style={{ display: "block" }} to={`/regions`}>
{item.name}
</Link>
</div>
))}
</div>
<div
style={{ marginTop: "20px", display: "flex", justifyContent: "center" }}
>
{countries && countries.length > 10 ? (
<button className="secondary-button">Load More</button>
) : (
<p>There are no more countries</p>
)}
</div>
</div>
);
}
Now from this component I need to pass the data of selected country id while the user clicks on the Link of the respective country, which I will be able to get by {item.code}. Also on clicking the Link the user will be redirected to /regions route where the list of regions of the selected country from this component will be shown. This is the RegionList Component:
import React from "react";
import { Link } from "react-router-dom";
import { BsSearch } from "react-icons/bs";
export default function RegionList(): JSX.Element {
return (
<div style={{ marginTop: "3rem" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "10px",
}}
>
<div>List of regions</div>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ position: "relative", marginRight: "1rem" }}>
<input
type="text"
placeholder="Filter"
style={{ padding: "0.35rem" }}
/>
<div style={{ position: "absolute", top: "5px", right: "5px" }}>
<BsSearch size="16" />
</div>
</div>
<div style={{ width: "8rem" }}>
<select name="sort" id="sort">
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
</div>
</div>
</div>
<div className="country-list-items">
<div>
<Link style={{ display: "block" }} to={`/cities`}>
Alaska
</Link>
</div>
</div>
<div
style={{ marginTop: "20px", display: "flex", justifyContent: "center" }}
>
<button className="secondary-button">Load More</button>
<p>There are no more countries</p>
</div>
</div>
);
}
I need to pass the country id from the CountryList component to this RegionList component because I will do a GET network call in the RegionList component using the selected country id passed from the CountryList component. But I am not able to pass the country id data from CountryList component to RegionList component as they are on different routes and they do not have any common parent component. This is the route file for Countries
import { Route, Routes } from "react-router-dom";
import React from "react";
import CountryComponent from "../components/CountryComponent";
export class CountryRoute extends React.Component {
render() {
return (
<Routes>
<Route path="/" element={<CountryComponent />} />
</Routes>
);
}
}
here <CountryComponent /> is the mother component of CountryList
This is the route file for Regions:
import { Route, Routes } from "react-router-dom";
import React from "react";
import RegionComponent from "../components/RegionComponent";
export class RegionsRoute extends React.Component {
render() {
return (
<Routes>
<Route path="/" element={<RegionComponent />} />
</Routes>
);
}
}
here <RegionComponent /> is the mother component of RegionList
Here is the Main Component where all the components are called
import React from "react";
import { Routes, Route } from "react-router-dom";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import styled from "styled-components";
import "styled-components/macro";
import { CountryRoute } from "../country/route";
import { RegionsRoute } from "../region/route";
import { CitiesRoute } from "../cities/route";
const MainContainer = styled.div`
min-height: 100%;
margin: 5rem;
`;
export const Main = (): JSX.Element => {
return (
<>
<>
<MainContainer>
<div style={{ textAlign: "center" }}>
<b>GEO SOFTWARE</b>
</div>
<div>
<div>
<Routes>
<Route path={"/countries*"} element={<CountryRoute />} />
<Route path={"/regions*"} element={<RegionsRoute />} />
<Route path={"/cities*"} element={<CitiesRoute />} />
</Routes>
</div>
</div>
<ToastContainer
toastClassName={"toastContainer e-12"}
hideProgressBar
position="bottom-left"
closeButton={false}
autoClose={5000}
bodyClassName={"toastBody"}
/>
</MainContainer>
</>
</>
);
};
Now how can I pass the selected country code data from CountryList to the RegionList component.
You can use Query Params for this. In the CountryList you can use the Link like this:
<Link style={{ display: "block" }} to={`/regions?country=COUNTRY_ID`}>
Then in the RegionsList youn can get that Query Parameter from the url and use as you want.
Check this example https://reactrouter.com/web/example/query-parameters
You could set up a simple "store" to keep track of the selected country independently of your component hierarchy.
The simplest possible store
A stripped down, simplest implementation possible might look something like this:
const data = {}
export default {
setCountry: c => data.country = c,
getCountry: () => data.country
}
Because the "store" data is a singleton, any component that imports the store will get the same info, regardless of where it is in the component tree.
import store from './store';
export default () => (
<div>{store.getCountry()}</div>
)
Listening for changes, etc.
The example above omits some details that may be important, depending on what you're doing, like updating views that have already rendered when the country value changes.
If you need that sort of thing you could make the store an event emitter so your components can listen for updates:
import Emitter from 'events';
class CountryStore extends Emitter {
data = {}
getCountry () {
return this.data.country;
}
setCountry (c) {
this.data.country = c;
this.emit('change'); // notify interested parties of the change
}
}
export default new CountryStore();
With the emitter in place, components can register for change notifications when they mount:
import store from './store';
function SomeComponent () {
useEffect(() => {
store.on('change', () => {
// do stuff when store changes happen
}, [])
})
return (<div>...</div>)
}
Custom Hook
To make it easy to do this wherever its needed you could wrap it all up in a custom hook that handles it all and returns the current value and a setter [country, setCountry] just like useState would:
const useCountry = () => {
const [country, setCountry] = useState(store.getCountry());
const handler = () => setCountry(store.getCountry());
useEffect(() => {
store.on('change', handler);
return () => store.off('change', handler);
})
return [country, c => store.setCountry(c)];
}
Then your components have it easy:
import useCountry from './useCountry.js';
export default function SomeComponent () {
const [country, setCountry] = useCountry();
return (
<div>
<div>Current Country: {country}</div>
<button onClick={() => setCountry(Math.random())}>Change Country</button>
</div>
)
}
There are off-the-shelf libraries that will do all of this and more for you, but I thought it might be more helpful to explain an actual rudimentary implementation.
You can have some sort of global state country_id which is initially equal to null.
When user clicks on a country, set that country_id to be equal to the clicked country id.
Now, Inside you RegionList component you can access the country id through country_id state.
You can achieve the state management by different ways:
Prop drilling
Context API
Use Redux or Recoil to handle state-management
As others have pointed out, this is 100% what context is for.
It looks like this:
import React, { createContext, useContext } from 'react';
const MyCountryContext = createContext(null);
export const useCountry = () => useContext(MyCountryContext);
export const MyCountryContext = ({children}) => {
const [country,setCountry] = useState();
return (
<MyCountryContext.Provider value={[country,setCountry]}>
{children}
</MyCountryContext.Provider>
)
}
Use it like this:
export const Main = (): JSX.Element => {
return (
<MyCountryContext>
...rest of your tree
</MyCountryContext>
);
}
Then, in any components that are below MyCountryContext you can use the hook just like useState:
import { useCountry } from './MyCountryContext';
const MyComponentThatUsesCountry = () => {
const [country,setCountry] = useCountry();
return (...)
}

How to add item to list

MainPage
export class Diet extends Component {
constructor(props) {
super(props);
this.state = {
list: [],
};
}
addToList(item) {
const list = [...this.state.list, item];
this.setState({ list });
}
render() {
return (
<View>
<Text style={styles.txtYourMeals}>Your Meals</Text>
<FoodList items={this.state.list} /> <--------
</View>
);
}
}
export default Diet;
FoodList
import React, { Component } from "react";
export class FoodList extends Component {
render() {
return (
<View>
<Content>
<List>
<ListItem>
<Text>FoodCreated</Text>
</ListItem>
</List>
</Content>
</View>
);
}
}
export default FoodList;
FoodCreate
export default function FoodCreate({ navigation: { goBack } }) {
const [FoodName, setFoodName] = useState("");
return (
<Container>
<Header>
<Left>
<Button transparent>
<Icon
name="arrow-back"
onPress={() => goBack()}
style={{ fontSize: 25, color: "red" }}
/>
</Button>
</Left>
<Body>
<Title>Add Food</Title>
</Body>
<Right>
<Button transparent>
<Icon <-----------
name="checkmark"
style={{ fontSize: 25, color: "red" }}/>
</Button>
</Right>
</Header>
<TextInput
placeholder="Food Name"
placeholderTextColor="white"
style={styles.inptFood}
value={FoodName}
onChangeText={(FoodName) => setFoodName(FoodName)}
/>
</Container>
);
}
So I'm trying to let the user type a Food Name in a TextInput in the FoodCreate page and pressing the button checkmark to add that food name in the FoodList which is displayed in the MainPage. I started but I have no idea on how to proceed. It's a basic grocery shopping list in which you type a food name and add it to your list and every time you do that you insert a new item.
What you need is to tell the parent component i.e. MainPage that a new food item has been created.
This should look something like this in your MainPage
render() {
return (
<>
<FoodCreate addToList={addToList}/>
<View>
<Text style={styles.txtYourMeals}>Your Meals</Text>
<FoodList items={this.state.list} /> <--------
</View>
);
}
Now, this addToList would be available to your FoodCreate component which you can call whenever you create the food item. Also, I don't see any Save button in your FoodCreate. I think that's where you might want to add a click listener so that whenever user clicks on that button, you call the addToList method

1 All instances of a component reference the first instance instead of itself; 2 Children props get updated only after calling parent's setState twice

UPDATE: I easily fixed the first issue by providing unique names for all the 'Rating' components, but the question about 2 x setState fixing props remains open.
I actually have two questions. The second one emerged from trying to solve the first one.
The initial problem was that 'this' in each of the onChange Rating's property in each of the SongScoring components pointed to the first instance of the SongScoring class instead of itself:
Parent code:
import React, { Component } from 'react';
import './Assess.css';
import { Accordion } from 'react-bootstrap';
import SongScoring from './SongScoring';
class Assess extends Component {
constructor(props) {
super(props);
const songs = [
{
track: "song1",
},
{
track: "song2",
},
{
track: "song3",
},
{
track: "song4",
},
]
this.state = {
songs: songs,
};
}
render() {
return (
<div className="root">
<Accordion defaultActiveKey="0">
{
this.state.songs.map((song, index) => (
<SongScoring song={song} key={index.toString()} index={index} />
))
}
</Accordion>
</div>
);
}
}
export default Assess;
Child code:
import React, { Component } from 'react';
import './SongScoring.css';
import { Accordion, Card, Container, Row } from 'react-bootstrap';
import ReactPlayer from "react-player";
import Rating from '#material-ui/lab/Rating';
import Box from '#material-ui/core/Box';
const labels = {
0.5: 'Unpleasant',
1: 'Bearable',
1.5: 'Bearable+',
2: 'Intriguing',
2.5: 'Intriguing+',
3: 'Ok',
3.5: 'Ok+',
4: 'Pleasant',
4.5: 'Pleasant+',
5: 'Excellent',
};
class SongScoring extends Component {
constructor(props) {
super(props);
this.state = {
song: props.song,
key: props.index.toString(),
score: props.song.score || 0,
hover: props.song.score || -1,
onChange: this.props.onChange,
onChangeActive: this.props.onChangeActive
}
}
render() {
return (
<>
<Card>
<Accordion.Toggle as={Card.Header} variant="link" eventKey={this.state.key}>
{this.state.key}
<Rating name="read-only" value={this.state.score} precision={0.5} readOnly />
</Accordion.Toggle>
<Accordion.Collapse eventKey={this.state.key}>
<Card.Body style={{ display: 'flex', alignItems: 'center', lineHeight: '1' }}>
<Container>
<Row className='scoring-row'>
<ReactPlayer
url="https://file-examples-com.github.io/uploads/2017/11/file_example_MP3_700KB.mp3"
width="400px"
height="50px"
playing={false}
controls={true}
style={{ outline: 'none' }}
/>
</Row>
<Row className='scoring-row'>
<Rating
name="song-rating"
value={this.state.score}
precision={0.5}
onChange={(event, newScore) => {
this.setState({
score: newScore
});
}}
onChangeActive={(event, newHover) => {
this.setState({
hover: newHover
});
}}
style={{ marginTop: '40px' }}
/>
</Row>
<Row className='scoring-row'>
{this.state.score !== 0 && <Box>{labels[this.state.hover !== -1 ? this.state.hover : this.state.score]}</Box>}
</Row>
</Container>
</Card.Body>
</Accordion.Collapse>
</Card>
</>
);
}
}
export default SongScoring;
After multiple iterations of trying to solve this problem by changing from arrow functions and binding, putting onChange and onChangeActive into the state etc. I finally decided to try to solve this problem by passing the onChange and onChangeActive functions from the parent to the child.
This is what I came up with (there is console.log instead of setState in onChange and onChangeActive, but what we care about is 'this' reference really):
Parent code:
import React, { Component } from 'react';
import './Assess.css';
import { Accordion } from 'react-bootstrap';
import SongScoring from './SongScoring';
class Assess extends Component {
constructor(props) {
super(props);
const songs = [
{
track: "song1",
},
{
track: "song2",
},
{
track: "song3",
},
{
track: "song4",
},
]
this.songScoring = [];
const onChange = [];
const onChangeActive = [];
for (let i = 0; i < songs.length; i++) {
this.songScoring.push(React.createRef());
onChange.push(function (event, newScore) {
console.log("onChange: ", this);
});
onChangeActive.push(function (event, newHover) {
console.log("onChangeActive: ", this);
});
}
this.state = {
songs: songs,
onChange: onChange,
onChangeActive: onChangeActive
};
}
componentDidMount() {
console.log(this.state.songScoring);
const onChange = [];
const onChangeActive = [];
for (let i = 0; i < this.state.songs.length; i++) {
onChange.push(this.state.onChange[i].bind(this.songScoring[i], 1));
onChangeActive.push(this.state.onChangeActive[i].bind(this.songScoring[i]));
}
this.setState({
onChange: onChange,
onChangeActive: onChangeActive
});//, () => this.setState({unicorn: 1}));
}
render() {
return (
<div className="root">
<Accordion defaultActiveKey="0">
{
this.state.songs.map((song, index) => (
<SongScoring song={song} ref={this.songScoring[index]} key={index.toString()} index={index} onChange={this.state.onChange[index]} onChangeActive={this.state.onChangeActive[index]} />
))
}
</Accordion>
</div>
);
}
}
export default Assess;
Child code:
import React, { Component } from 'react';
import './SongScoring.css';
import { Accordion, Card, Container, Row } from 'react-bootstrap';
import ReactPlayer from "react-player";
import Rating from '#material-ui/lab/Rating';
import Box from '#material-ui/core/Box';
const labels = {
0.5: 'Unpleasant',
1: 'Bearable',
1.5: 'Bearable+',
2: 'Intriguing',
2.5: 'Intriguing+',
3: 'Ok',
3.5: 'Ok+',
4: 'Pleasant',
4.5: 'Pleasant+',
5: 'Excellent',
};
class SongScoring extends Component {
constructor(props) {
super(props);
this.state = {
song: props.song,
key: props.index.toString(),
score: props.song.score || 0,
hover: props.song.score || -1,
onChange: this.props.onChange,
onChangeActive: this.props.onChangeActive
}
}
componentDidUpdate(prevProps, prevState){
if (prevProps.onChange !== this.state.onChange){
this.setState({
onChange: prevProps.onChange,
onChangeActive: prevProps.onChangeActive
})
}
}
render() {
return (
<>
<Card>
<Accordion.Toggle as={Card.Header} variant="link" eventKey={this.state.key}>
{this.state.key}
<Rating name="read-only" value={this.state.score} precision={0.5} readOnly />
</Accordion.Toggle>
<Accordion.Collapse eventKey={this.state.key}>
<Card.Body style={{ display: 'flex', alignItems: 'center', lineHeight: '1' }}>
<Container>
<Row className='scoring-row'>
<ReactPlayer
url="https://file-examples-com.github.io/uploads/2017/11/file_example_MP3_700KB.mp3"
width="400px"
height="50px"
playing={false}
controls={true}
style={{outline: 'none'}}
/>
</Row>
<Row className='scoring-row'>
<Rating
name="song-rating"
value={this.state.score}
precision={0.5}
onChange={this.state.onChange}
onChangeActive={this.state.onChangeActive}
style={{ marginTop: '40px' }}
/>
</Row>
<Row className='scoring-row'>
{this.state.score !== 0 && <Box>{labels[this.state.hover !== -1 ? this.state.hover : this.state.score]}</Box>}
</Row>
</Container>
</Card.Body>
</Accordion.Collapse>
</Card>
</>
);
}
}
export default SongScoring;
In this setting, console.log in onChange and onChangeActive output both 'undefined'.
Now, if I uncomment the fragment: '//, () => this.setState({unicorn: 1}));' (unicorn is a dummy, unused variable) in the parent, console.log of onChange and onChangeActive prints out nicely 'this' as the references for respective SongScoring components.
Therefore my questions are:
what's going on here, in both problems? Is it a bug of the 'Rating' component?
how to solve my initial problem more efficiently than by using the second approach with the uncommented fragment?
UPDATE: I easily fixed the first issue by providing unique names for all the 'Rating' components, but the question about 2 x setState fixing props remains open.

Passing props to children in React

I'm trying to make a Formik wrapper which takes children as props and would render anything put inside. There are a couple forms to make which take different initial values and validation schema etc. The only thing in common thing is the grid layout. The goal is to have the access to Formik props like values, errors etc. in the child component and I have no idea how to pass it to its child. The form fields don't even show up.
The wrapper:
import React from 'react';
import { Formik, FormikConfig, FormikValues } from "formik";
import { Col, Layout, Row } from "antd";
const FormContainer: React.FC<FormikConfig<FormikValues>> = ({ children, ...props }) => {
return <Formik
{...props}
>
{props => (
<Layout>
<Row style={{ height: "100vh", display: "flex", alignItems: "center" }}>
<Col span={12}>
<Layout>
{/*this will be replaced with some background image*/}
<pre>{JSON.stringify(props.values, null, 2)}</pre>
<pre>{JSON.stringify(props.errors, null, 2)}</pre>
</Layout>
</Col>
<Col span={12}>
<Layout>
{/*here goes goes a Form from a different components*/}
{children}
</Layout>
</Col>
</Row>
</Layout>
)}
</Formik>
};
export default FormContainer;
I must be doing something wrong. I am unable to get any Formik props/values from anywhere else when I wrap FormContainer around anything.
My form example (so far):
import React from "react";
import { Field, Form } from "formik";
import { Col, Form as AntForm, Icon, Input, Row } from "antd";
import { initialValues, validationSchema } from "./fieldValidation";
import FormContainer from "../../../containers/FormContainer/FormContainer";
const RegisterPage: React.FC = () => {
return (
<FormContainer
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={(data, { setSubmitting }) => {
setSubmitting(true);
setTimeout(() => {
alert(JSON.stringify(data, null, 2));
setSubmitting(false);
}, 5000);
}}
>
{({touched, errors}) => (
<Form>
<Row gutter={[8, 8]}>
<Col span={12}>
<AntForm.Item
help={touched.firstName && errors.firstName ? errors.firstName : ""}
validateStatus={touched.firstName && errors.firstName ? "error" : undefined}
>
<Field
name="firstName"
prefix={<Icon type="solution" style={{ color: "rgba(0,0,0,.25)" }} />}
placeholder="First name"
as={Input}
/>
</AntForm.Item>
</Col>
<Col span={12}>
<AntForm.Item
help={touched.lastName && errors.lastName ? errors.lastName : ""}
validateStatus={touched.lastName && errors.lastName ? "error" : undefined}
>
<Field
name="lastName"
prefix={<Icon type="solution" style={{ color: "rgba(0,0,0,.25)" }} />}
placeholder="Last name"
as={Input}
/>
</AntForm.Item>
</Col>
</Row>
</Form>
)}
</FormContainer>
);
};
export default RegisterPage;
I'm stuck. What am I doing wrong here?
Here's how to pass the prop "propsToPass" from the parent to all his direct children:
const Parent = props => {
const { children } = props;
const childrenWithExtraProp = React.Children.map(children, child =>
React.cloneElement(child, { propsToPass: "toChildren" })
);
return <div>{childrenWithExtraProp}</div>;
};
export default Parent;
So in this case, both children will have the prop "propsToPass"
<Parent>
{/* this.props.propsToPass will be available in this component */}
<Child></Child>
{/* this.props.propsToPass will be available in this component */}
<AnotherChild></AnotherChild>
</Parent>
You could do the same for your form.
I don't see like rendering Formik as children is good idea here, especially that you are supposed to render one form in such FormWrapper. I would use render props here, so here is basic example how you can do it.
Anyway, I still can't get your concept of re-inventing FormWrapper if Formik provides its own wrapper:
https://jaredpalmer.com/formik/docs/api/formik
interface FormWrapperProps extends FormikConfig<FormikValues> {
renderForm(props: FormWrapperProps): React.ReactNode
}
export const RegisterForm = (props: FormWrapperProps) => (
<form>
<input type="text"/>
<input type="text"/>
</form>
)
const FormWrapper: React.FC<FormWrapperProps> = (props) => {
return (
<div className="layout">
{/*here goes goes a Form from a different components*/}
{props.renderForm(props)}
</div>
)
}
const FormPage = () => {
const props = {} as FormWrapperProps
return (
<FormWrapper
{...props}
renderForm={(props: FormWrapperProps) => <RegisterForm {...props} />}
/>
)
}

Categories