How to pass a mutated state to a different page? - javascript

In my App.js, I'm creating a Route as such:
<Route
exact path='/tools/ndalink'
render={(props) => (
<React.Fragment>
<Header />
<LinkPage {...props} brokerID={this.state.brokerID}></LinkPage>
</React.Fragment>
)}
/>
state.brokerID is initially "", but changed shortly after, therefore LinkPage receives this.state.brokerID as "".
How can I pass the changed state.brokerID (without using Redux)?

You need to use a lifecycle method to get the props to the component to wait for the props called componentDidUpdate.
That being said, you only have to use this if you plan to mutate the brokerId.
Since the process is async you'll have to wait for the props to be passed down. Until the you can show a loading text or progess bar.
class LinkPage extends React.Component {
state = {
builderId: ''
};
componentDidUpdate(prevProps) {
if(this.props.builderId !== prevProps.brokerId) {
this.setState({ builderId: this.props.brokerId });
}
}
render() {
return (
<div className="App">
<h1>{ this.state.builderId ? this.state.builderId : 'Loading' }</h1>
</div>
);
}
}
Or, a simple method would be to not use the lifecycle method at all, Change the following line in render and it should work:
<h1>{ this.props.builderId ? this.props.builderId : 'Loading' }</h1>
If you need to use this brokerId for an api call or something, you can use the setState callback. This would go something like this in the componentDidUpdate lifecycle method.
componentDidUpdate(prevProps) {
if(this.props.builderId !== prevProps.builderId) {
this.setState({ builderId: this.props.builderId }, () => {
//use this.state.brokerIdhere
});

Related

React render JSX from method

In order to keep the render method of my component shorter and also avoid creating additional components I was hoping to render the HTML from a class method depending on the state like so:
class ExampleComponent extends React.Component {
constructor (props) {
super(props);
this.state = {
step: 'step1'
}
}
handleChangeStep(step) {
this.setState({ step: step })
}
step1 () {
return (
<>
<h2>Step 1</h2>
<div onClick={this.handleChangeStep('step2')}>Next Step</div>
</>
)
}
step2 () {
return (
<>
<h2>Step 2</h2>
<div onClick={this.handleChangeStep('step1')}>Previous Step</div>
</>
)
}
render () {
return this.state.step === 'step2' ? this.step2() : this.step1();
}
}
However this gives the error: Warning: Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state..
Seems you can't do this and instead need to do something like (from: https://reactjs.org/docs/conditional-rendering.html)
return this.state.step === 'step2' ? <Step1 /> : <Step2 />
However I don't understand why the former example isn't allowed as those two methods just return JSX so in theory should allow you to return one or the other depending on the state.
The problem has nothing to do with what you're describing. You're trying to update state during a render:
onClick={this.handleChangeStep('step2')}
When this.handleChangeStep('step2') is invoked, state is updated. Updating state triggers a re-render. Which in this case would then update state again, and again, indefinitely.
I suspect you meant to pass a function reference to onClick, not invoke a function:
onClick={() => this.handleChangeStep('step2')}

How to re-render a component with React-Router <Link> pointing to the same URL

To keep it simple, the detail page fetches data on mount based on the movie ID in the URL, this coming from path='movie/:id' in the Route.
It's child is called Recommended, which shows you recommended movies based again on the current URL.
class MovieDetailPage extends React.Component {
// Fetch movies and cast based on the ID in the url
componentDidMount() {
this.props.getMovieDetails(this.props.match.params.id)
this.props.getMovieCast(this.props.match.params.id)
}
render() {
<div>
Movies here
</div>
<Recommended id={this.props.match.params.id}/>
}
}
The Recommended component fetches data based on the current movie as well and generates another tag pointing to another movie.
class Recommended extends React.Component {
componentDidMount() {
this.props.getRecommended(this.props.id)
}
render() {
return (
<>
<Category title={'Recommended'}></Category>
<div className="movies">
{
this.props.recommended.map((movie) => {
return (
<Link key={movie.id} to={`movie/${movie.id}`} className="movies__item">
<img
key={movie.id}
src={`https://image.tmdb.org/t/p/w342${movie.poster_path}`}
className="movies__item-img"
alt={`A poster of ${movie.title}`}
>
</img>
</Link>
)
})
}
</div>
</>
)
}
}
Now how can I trigger another render of the parent component when clicking the Link generated in the Recommended component? The URL is changing but this won't trigger a render like I intent to do.
UPDATE:
<Route
path="/movie/:id"
render={(props) => (
<MovieDetailPage key={props.match.params.id}
{...props}
)}
/>
I passed in a unique key this time that triggered the re-render of the page. I tried this before but I might've screwed up the syntax.
This post got me in the right direction: Force remount component when click on the same react router Link multiple times
Add a key to the page
If you change route but your page is not getting its "mount" data then you should add a key to the page. This will cause your page to rerender and mount with the new id and get the data again.
You can read more about react keys here
A key tells react that this is a particular component, this is why you see them in on lists. By changing the key on your page you tell react that this is a new instantiation of the component and has changed. This will cause a remount.
Class component example
class MyPage extends React.Component {
componentDidMound() {
// this will fire each time the key changes since it triggers a mount
}
render() {
return (
<div key={props.pageId}>
{/* component stuff */}
</div>
)
}
}
Functional component example
const MyPage = (props) => {
React.useEffect(() => {
// this will fire each time the key changes
}, []);
return (
<div key={props.pageId}>
{/* component stuff */}
</div>
)
}
You can add another React lifecycle method that triggers on receiving new props (UNSAFE_componentWillReceiveProps, componentDidUpdate, getDerivedStateFromProps) in your Recommended component like this:
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.id !== this.props.id) {
nextProps.getRecommended(nextProps.id);
};
}
You can also add key to your component (which forces it to re-render completely if key changed) like this:
<Recommended key={this.props.match.params.id} id={this.props.match.params.id}/>
You can also use React Hooks to handle this more easily with useEffect:
const Recommended = (props) => {
const { id, getRecommended, recommended } = props;
useEffect(() => {
id && getRecommended(id);
}, [id]);
return (
<>
<Category title={'Recommended'}></Category>
<div className="movies">
{recommended.map((movie) => {
return (
<Link key={movie.id} to={`movie/${movie.id}`} className="movies__item">
<img
key={movie.id}
src={`https://image.tmdb.org/t/p/w342${movie.poster_path}`}
className="movies__item-img"
alt={`A poster of ${movie.title}`}
></img>
</Link>
);
})}
</div>
</>
);
};
Note: adding key to component and complete its re-render is not best practice and you should be using Component's lifecycles to avoid it if possible

Passing value from one component to other component

I have a query.. I need to pass the value from one component to other component.. I want the value received (view console) after click event in the breedlist.js component to replace the base url value "akita" value in the breedimages.js
Refer to:
https://stackblitz.com/edit/react-kfwzgi
First and foremost, you will need to define additional props on both < <BreedImages /> and <BreedList /> components to handle the receiving of selected breeds, and selecting/clicking of breeds, respectively.
On your index.js,
const BASE_URL= "https://dog.ceo/api/breeds/list/all"
class App extends Component {
state={
breeds: [],
selectedBreed: undefined,
}
componentDidMount(){
fetch(BASE_URL)
.then(res => res.json())
.then(data => {
this.setState({
breeds: Object.keys(data.message)
})
})
}
handleSelect(selectedBreed) {
this.setState({ selectedBreed });
}
render() {
const { selectedBreed } = this.state;
return (
<Router>
<div className="App">
<Route path="/" exact strict render={
()=> {
return (<BreedList handleSelect={(val) => this.handleSelect(val)}/>)
}
}/>
<Route path="/images/" exact strict render={
()=> {
return (<BreedImages breed={selectedBreed}/>)
}
}/>
</div>
</Router>
);
}
}
Then, on your BreedList.js, you will need to modify the clickHandler method such that it is calling the handleSelect props method, to pass the selected dog breed back to the parent index.js component,
clickHandler(e) {
const val = e.currentTarget.value
this.props.handleSelect(val)
}
Then, on your BreedImages.js, you can listen to the prop updates on the componentDidUpdate lifecycle hook, and carry out the necessary operations from there.
I added an if statement to compare the prop values before doing any additional operations, as this will prevent any unnecessary re-rendering or requests.
componentDidUpdate(props, prevProps){
if (props.breed !== prevProps.breed) {
console.log(props);
// handle the rest
}
}
I have forked your demo and made the changes over here.

Is React context provider not mounted before child components?

How do I make sure I set a value in the context provider before components are mounted?
In the code example below, the console.log in the child component(Dashboard) will be logged first (as undefined). Why is that and is there any way for me to make sure the value is set before that component is mounted?
App.js
render() {
return (
<div className='App'>
<ContextProvider>
<Dashboard />
</ContextProvider>
</div>
);
}
ContextProvider.js
componentDidMount = () => {
this.setState({value: 1})
console.log(this.state.value);
}
Dashboard.js
componentDidMount = () => {
console.log(this.context.value);
}
Children are rendered first. Regardless of that, setState is asynchronous, so a context will be provided to consumers asynchronously.
In case there's a necessity for children to wait for a context, they should be either conditionally rendered:
render() {
this.context.value && ...
}
Or be wrapped with context consumer which can be written as a HOC for reuse:
const withFoo = Comp => props => (
<FooContext.Consumer>{foo => foo.value && <Comp {...props}/>}</FooContext.Consumer>
);

Child component not rerendering on change of props

Here is the parent component :
state = {
books: undefined
}
componentDidMount() {
BooksAPI.getAll().then(books => {
this.setState({books},this.filterBooks);
});
}
filterBooks() {
this.currentlyReading = this.state.books.filter((book) => book.shelf === 'currentlyReading');
this.read = this.state.filter((book) => book.shelf === 'read');
this.wantToRead = this.state.filter((book) => book.shelf === 'wantToRead');
}
render() {
return (
<div className="app">
<Route path="/search" component={SearchBook} />
<Route exact
path="/"
render={() => <BookScreen
currentlyReading={this.currentlyReading}
read={this.read}
wantToRead={this.wantToRead}
/>} />
</div>
)
}
I expect that the props will change for BookScreen component after filterBooks is called and the component should rerender, but it doesn't. What am I doing wrong?
In my opinion, the best way for now, which still follows React's way is: after having books data, call 1 function only, and this function will process everything and update state after finishing (we pass the books object as parameter), like this:
componentDidMount() {
BooksAPI.getAll().then(books => {
this.filterBooks({books});
});
}
filterBooks = (books) => {
this.currentlyReading = books.filter((book) => book.shelf === 'currentlyReading');
this.read = books.filter((book) => book.shelf === 'read');
this.wantToRead = books.filter((book) => book.shelf === 'wantToRead');
this.setState({ books: books });
}
If you have any error, feel free to post here then we can get through together!
========
Added explanation on why the author's original code doesn't work:
Based on my little experience with React and JS:
When a new state is set, it may take time (maybe 100-300ms, that is why you execute the original this.filterBooks using the syntax this.setState({books},this.filterBooks)
=> This seems to be right, which means after a new state of books is already set, you can access it the filterBooks function.)
HOWEVER: after new state of books is set, the page will be re-rendered, and filterBooks will be executed (perhaps at the same time => not sure which one ends first, so let's say for example, this.currentlyReading is still undefined in render() if render() happens first, before the result of filterBooks is completely set!
In other words, React is Javascript, and Javascript's asynchronization is troublesome!
You can try to do this, Just update the books state like this:
componentDidMount() {
BooksAPI.getAll().then(books => this.setState({books}));
}
This will cause a re-render. But since you're not using the state directly to populate your child component, we need to call the filterBooks() inside the render() method.
render() {
this.filterBooks()
return (
<div className="app">
<Route path="/search" component={SearchBook} />
<Route exact
path="/"
render={() => <BookScreen
currentlyReading={this.currentlyReading}
read={this.read}
wantToRead={this.wantToRead}
/>} />
</div>
)
}
The call to that method will update the data that you pass as props to your child component.

Categories

Resources