React Router 4 - Controlled Component not Un-Mounting on Switch - javascript

I'm trying to learn how to lift the state from <Child/> to <Parent/> and have the parent control user interactions done the child, (which receives state down as a prop) i.e show color and text.
I was able to lift the state up. However, when I switch back and forth between routes the <Parent/> component is not re-mounting and its state remains exactly how it was previously set by setState({})
const cars = [
{ name: "Ferrari", cost: "$9.000", color: "red", id: 1 },
{ name: "Porsche", cost: "$8.000", color: "black", id: 2 },
***
];
class Dealership extends Component {
state = {
cars,
isShow: {},
correctIndex: Math.floor(Math.random() * (cars.length - 1))
};
handleShuffle = () => {
this.setState({
cars: [...this.state.cars.sort(() => Math.random() - 0.5)],
isShow: {}
});
};
handleShow = car => {
const { isShow } = this.state;
console.log("isShow=", isShow);
this.setState(currentState => ({
isShow: { ...currentState.isShow, [car]: true }
}));
};
render() {
return (
<>
<Navigation />
<Routes
state={this.state}
shuffle={this.handleShuffle}
handleShow={this.handleShow}
// isShow={this.state.isShow}
/>
</>
);
}
}
export default withRouter(Dealership);
As mentioned above, the child <Car/>is receiving state down as props so that its user interaction can be controlled by one source of truth the parent <Dealership />
export default class Car extends Component {
render() {
const { cars, shuffle, isShow, handleShow, correctIndex } = this.props;
const correctCar = cars[correctIndex];
const car = cars.map(car => (
<CarList
// {...this.state}
isShow={isShow[car.name]}
key={car.id}
car={car.name}
guess={car.cost}
isCorrect={correctCar.cost === car.cost}
handleShow={handleShow}
/>
));
return (
<>
<Question key={correctCar.id} guess={correctCar.cost} />
<button
onClick={() => {
shuffle();
}}
>
go again
</button>
<ul className="car-list">{car}</ul>
</>
);
}
}
The <CarList/> is abstracted here:
// CarList.js
export const CarList = ({ isShow, isCorrect, car, handleShow, guess }) => {
function getColor() {
if (isShow) {
const showColor = isCorrect ? "green" : "red";
return showColor;
}
return "";
}
return (
<li onClick={() => handleShow(car)} className={getColor()}>
{car}
<span className={isShow ? "show" : "hide"}>{guess}</span>
</li>
);
};
Oddly (to me), when I switch to a route that holds its own local state i.e <Bike/>, everything works as expected (the state is back to original)
import React, { useState } from "react";
export const Bike = () => {
const [color, setColor] = useState(false);
function ChangeColor() {
setColor(true);
}
return (
<p onClick={ChangeColor}>
Click on the <span className={color ? "red" : " "}>Bike</span>
</p>
);
};
This is how I have my Routes setup:
// Navigation.JS
export const Navigation = () => (
<nav>
<ul>
<li>
<Link to="/">home</Link>
</li>
<li>
<Link to="/car-cost">car</Link>
</li>
<li>
<Link to="/bike">bike</Link>
</li>
</ul>
</nav>
);
// Routes.js
export const Routes = ({ state, shuffle, handleShow, isShow }) => (
<Switch>
<Route
path="/car-cost"
render={() => (
<Car
{...state}
shuffle={shuffle}
handleShow={handleShow}
// isShow={isShow}
/>
)}
/>
<Route path="/bike" render={() => <Bike />} />
<Route path="/" component={Home} />
</Switch>
);
I then wrapped my main app with <BrowserRouter /> as you see in totality plus the current misbehavior happening on this code sandbox
How can I switch between routes having <Car/> behave such as <Bike/>? i.e return to its original state. Also, am I lifting and controlling state correctly here?

Here the state are being saved in parent component. When the route changes then only child components are being remounted. So the state of parent component remains there throughout that routing.
You can keep the state in child component, which would reset the state after every unmount. However if you want to lift the state up and still reset the state, then you would have to do that in parent component.
A better way would be to monitor the route change in the parent component. If the route has changed then parent component should reset its state. In componentDidUpdate method of parent component, you can track the route change and reset the state like this
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
console.log('Route change! Reset the state');
this.setState({ isShow: {}})
}
}

Related

How to add class to a passed Component

When trying to pass a component as a prop of another component, everything works fine.
But if i want instead pass a Component and handle its css classes inside the children, I'm currently lost.
In my mind im trying to achieve something similar to this:
import Navbar from 'what/ever/path/Navbar/is/in/Navbar.js';
export default function ParentComponent {
return(
<Navbar NavIcon={<MyIcon/>} />
)
}
.... Imports etc...
export default function Navbar(props) {
const {NavIcon} = props;
return(
<Navigation>
// Now use the Prop as a Component and pass default classNames to it.
// So that we don't need to wrap everything inside a span / div etc.
<NavIcon className="AddCustomStylesAlwaysHere" />
</Navigation>
)
}
Two approaches come to my mind:
Passing a component
Just pass the component and let the parent take care of its instantiation. This way, the only changes you need is making sure <MyIcon /> accepts a className prop:
const MyIcon = ({ className }) => {
return <div className={className} />
};
const Navbar = ({ NavIcon }) => {
return (
<Navigation>
<NavIcon className="AddCustomStylesAlwaysHere" />
</Navigation>
);
};
<Navbar NavIcon={MyIcon} />
Passing an element instance
This way, you take care of instantiating the component and the parent just renders it. In this case, you have to use React utilities to modify existing elements (https://reactjs.org/docs/react-api.html#cloneelement):
const MyIcon = ({ className }) => {
return <div className={className} />
};
const Navbar = ({ NavIcon }) => {
return (
<Navigation>
{React.cloneElement(NavIcon, { className: 'AddCustomStylesAlwaysHere' })}
</Navigation>
);
};
<Navbar NavIcon={<MyIcon />} />
You can use React.Children.map in combination with React.cloneElement:
{
React.Children.map(children, ( child, idx ) => {
return React.cloneElement(child, { className: 'additional-classnames' })
})
}

How do you update state in functional components in React?

I'm running into the issue where I have created a functional component to render a dropdown menu, however I cannot update the initial state in the main App.JS. I'm not really sure how to update the state unless it is in the same component.
Here is a snippet of my App.js where I initialize the items array and call the functional component.
const items = [
{
id: 1,
value:'item1'
},
{
id: 2,
value:'item2'
},
{
id: 3,
value:'item3'
}
]
class App extends Component{
state = {
item: ''
}
...
render(){
return{
<ItemList title = "Select Item items= {items} />
And here is my functional componenet. Essentially a dropdown menu from a YouTube tutorial I watched (https://www.youtube.com/watch?v=t8JK5bVoVBw).
function ItemList ({title, items, multiSelect}) {
const [open, setOpen] = useState (false);
const [selection, setSelection] = useState([]);
const toggle =() =>setOpen(!open);
ItemList.handleClickOutside = ()=> setOpen(false);
function handleOnClick(item) {
if (!selection.some(current => current.id == item.id)){
if (!multiSelect){
setSelection([item])
}
else if (multiSelect) {
setSelection([...selection, item])
}
}
else{
let selectionAfterRemoval = selection;
selectionAfterRemoval = selectionAfterRemoval.filter(
current =>current.id == item.id
)
setSelection([...selectionAfterRemoval])
}
}
function itemSelected(item){
if (selection.find(current =>current.id == item.id)){
return true;
}
return false;
}
return (
<div className="dd-wraper">
<div tabIndex={0}
className="dd-header"
role="button"
onKeyPress={() => toggle(!open)}
onClick={() =>toggle(!open)}
onChange={(e) => this.setState({robot: e.target.value})}
>
<div className="dd-header_title">
<p className = "dd-header_title--bold">{title}</p>
</div>
<div className="dd-header_action">
<p>{open ? 'Close' : 'Open'}</p>
</div>
</div>
{open && (
<ul className ="dd-list">
{item.map(item =>(
<li className="dd-list-item" key={item.id}>
<button type ="button"
onClick={() => handleOnClick(item)}>
<span>{item.value}</span>
<span>{itemSelected(item) && 'Selected'}</span>
</button>
</li>
))}
</ul>
)}
</div>
)
}
const clickOutsideConfig ={
handleClickOutside: () => RobotList.handleClickOutside
}
I tried passing props and mutating the state in the functional component, but nothing gets changed. I suspect that it needs to be changed in the itemSelected function, but I'm not sure how. Any help would be greatly appreciated!
In a function component, you have the setters of the state variables. In your example, you can directly use setOpen(...) or setSelection(...). In case of a boolean state variable, you could just toggle by using setOpen(!open). See https://reactjs.org/docs/hooks-state.html (Chapter "Updating State") for further details.
So you need to do something like below . Here we are passing handleChange in parent Component as props to the child component and in Child Component we are calling the method as props.onChange
Parent Component:
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = {
value :''
}
}
handleChange = (newValue) => {
this.setState({ value: newValue });
}
render() {
return <Child value={this.state.value} onChange = {this.handleChange} />
}
}
Child Component:
function Child(props) {
function handleChange(event) {
// Here, we invoke the callback with the new value
props.onChange(event.target.value);
}
return <input value={props.value} onChange={handleChange} />
}

DOM render not triggering after updating hooks

I am trying to delete an item (const removeItem) from a list using an onClick event in React.
The state is managed with hooks. I know my way of deleting the item is not the right way yet (i'm putting the name to null), but this is not the issue.
After i set a user to null (i update the users object), i expect a render to happen (useEffect) but it does not. If i switch components and go back to this one, it works, but when clicking the X button, nothing happens in the view.
component Home:
import React, { Suspense, useState, useEffect } from "react";
const Home = props => {
const [users, setUsers] = useState(props.users);
console.log(users);
useEffect(() => {}, [users]);
const addItem = e => {
users.push(e);
console.log(e);
e.preventDefault();
};
const removeItem = item => {
users.forEach(user => {
if (user.id === item) {
user.name = null;
}
});
console.log(users);
setUsers(users);
};
return (
<div className="Home">
<form className="form" id="addUserForm">
<input
type="text"
className="input"
id="addUser"
placeholder="Add user"
/>
<button className="button" onClick={addItem}>
Add Item
</button>
</form>
<ul>
{users.map(item => {
return (
<>
<li key={item.id}>{item.name + " " + item.count}</li>
<button className="button" onClick={() => removeItem(item.id)}>
X
</button>
</>
);
})}
</ul>
</div>
);
};
export default Home;
How i get my users object:
import React from "react";
const LotsOfUsers =[...Array(100).keys()].map((item, key) => item = {
name : `User ${key}`,
id: key,
count: 0
})
export default LotsOfUsers;
main App:
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useRouteMatch,
useParams
} from "react-router-dom";
import Home from "./Home"
import CTX from './store'
import LotsOfUsers from "./LotsOfUsers";
export default function App() {
return (
<CTX.Provider value={{}}>
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/topics">
<Topics />
</Route>
<Route path="/">
<Home users={LotsOfUsers} text="hello world"/>
</Route>
</Switch>
</div>
</Router>
</CTX.Provider>
);
}
function About() {
return <h2>About</h2>;
}
function Topics() {
let match = useRouteMatch();
return (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/components`}>Components</Link>
</li>
<li>
<Link to={`${match.url}/props-v-state`}>
Props v. State
</Link>
</li>
</ul>
{/* The Topics page has its own <Switch> with more routes
that build on the /topics URL path. You can think of the
2nd <Route> here as an "index" page for all topics, or
the page that is shown when no topic is selected */}
<Switch>
<Route path={`${match.path}/:topicId`}>
<Topic />
</Route>
<Route path={match.path}>
<h3>Please select a topic.</h3>
</Route>
</Switch>
</div>
);
}
function Topic() {
let { topicId } = useParams();
return <h3>Requested topic ID: {topicId}</h3>;
}
Looking into your code, I've noticed 2 times that you change users without the use of setUsers.
const addItem = e => {
users.push(e); // <--- here
console.log(e);
e.preventDefault();
};
const removeItem = item => {
users.forEach(user => {
if (user.id === item) {
user.name = null; // <--- here
}
});
console.log(users);
setUsers(users);
};
In that way, you are updating your users, without letting react know about it. On both cases you have to update users with setUsers and not mutating the array directly.
const addItem = e => {
setUsers(users.concat(e)); // sidenote: if e is your event, then you might be looking for e.target.value here
console.log(e);
e.preventDefault();
};
const removeItem = item => {
setUsers(users.filter(user => user.id !== item));
};
Both .concat() and .filter() use your users array, and return a new array based on the changes you want to apply, and then is used by setUsers to update your users.
So, in extend what I'm actualy doing on removeItem is:
const removeItem = item => {
const newUsers = users.filter(user => user.id !== item); // users remains untouched
setUsers(newUsers);
};
Also, you don't need useEffect hook for this scenario to work properly.
I hope this solves your problem.
You are committing THE Fundamental sin of the react universe. "Mutation"
of state.
const removeItem = item => {
users.forEach(user => {
if (user.id === item) {
user.name = null; // <--- HERE
}
});
console.log(users);
setUsers(users);
};
Check this codesandbox for a working demo of this issue.
https://codesandbox.io/s/jovial-panini-unql4
The react reconciler checks to see whether the 2 objects are equal and since you have just mutated and set the value it registers as the same object and there will be no state change triggered. Hence the view will not be re-rendered and useEffect will not be triggered.

Passing props to nested child component

Here's my structure :
Main.js (Parent)
MainContainer.js
|
|_ Article.js
|
|__ Comments.js
Now i want to set click handler on comment component (recursive component) and dispatch an action.
here's my code on comment.js
class Comment extends Component {
deleteComment = (id) => {
this.props.handleDelete(id)
}
render() {
var comment = this.props.comment
return (
<div className={styles.commentsWrapper}>
<ul>
<li>
<div className={styles.commentsName}>
<a onClick={() => this.deleteComment(comment.id)} className={styles.commentsNameRight}>
</a>
</div>
<p>{comment.body}</p>
{comment.children.length > 0 && comment.children.map(function(child) {
return <Comment comment={child} key={child.id}/>
})}
</li>
</ul>
</div>
);
}
}
export default Comment;
and Article.js :
class Article extends Component {
handleDeleteComment = (id) => {
this.props.deleteComment(id)
}
render() {
return (
<article className={styles.articleItem}>
{this.props.comments.map(item =>
<Comment handleDelete={this.handleDeleteComment} comment={item} key={item.id}/>)}
</article>
);
}
}
export default Article;
And the Main.js
class Main extends Component {
deleteComment = (id) => {
this.props.deleteCommentRequest(id)
}
render() {
return (
<div className="">
<Header />
<section className="container">
<div>
{
!this.props.articles.loading && this.props.articles.articles? (
<div>
{this.props.articles.articles.map(item =>
<Article
bodytext={item.selftext}
key={item.id}
comments={item.finalComments}
deleteComment={this.deleteComment}
/>)}
</div>
) : (
<div className={styles.loading}> <Spin /> </div>
)
}
</div>
</section>
</div>
);
}
}
export default Main;
so what i did here is: pass deleteComment as props from main to article and pass again handleDelete from article to comment.
not sure if it's a good way of doing this ?
Thanks in advance
Nothing wrong with this pattern for 2 - 3 depth of components, as that is how data should flow from children to ancestors. But if your application is getting heavier with several layers, consider a different state management such as redux where a global state is maintained and any component can subscribe to it and dispatch actions. More on that here.
Alternatively you can also achieve the same with React Hooks with useContext where you can set the context and any child component can subscribe to it. Example:
const MyContext = React.createContext();
export default function App({ children }) {
const [items, setItems] = React.useState([]);
return (
<MyContext.Provider value={{ items, setItems }}>
{children}
</MyContext.Provider>
);
}
export { MyContext };
Now in any child at any level of depth as long as it is within App component's children, you can do this:
import {MyContext} from './filename';
function TodoItem() {
const { items, setItems } = React.useContext(MyContext);
return (
<div onClick={() => setItems(1)}>
</div>
);
}
you can use context API to have the props in the wrapper and easily accessible from child component.
there is a great tutorial from wesbos on youtube
class App extends Component {
render() {
return (
<MyProvider>
<div>
<p>I am the app</p>
<Family />
</div>
</MyProvider>
);
}
}
class MyProvider extends Component {
state = {
name: 'Wes',
age: 100,
cool: true
}
render() {
return (
<MyContext.Provider value={{
state: this.state,
growAYearOlder: () => this.setState({
age: this.state.age + 1
})
}}>
{this.props.children}
</MyContext.Provider>
)
}
}

React with react-router-dom 4 -- Cannot read property 'params' of undefined

I've been working on learning React to see if it suits my organization's needs, so needless to say I'm new at it. I've got a sample app that I've been working on to see how it works. I've gone through several of the answers here and haven't found one that fixes my problem.
I'm running into the problem where I get a "Uncaught (in promise) TypeError: Cannot read property 'params' of undefined" in the "componentDidMount()" at "const { match: { params } } = this.props;" method in the component below. I have a very similar component that takes an id from the url, using the same method, and it works fine. I'm confused as to why one is working and another isn't. I'm probably just making a rookie mistake somewhere (perhaps more than one), any hints/answers are appreciated.
The routing:
class App extends Component {
render() {
return (
<BrowserRouter>
<div>
<Route path='/' component={BaseView} />
<Route path='/test' component={NameForm} />
<Route path='/home' component={Home} />
<Route path='/quizzes' component={ViewQuizzes} />
<Route path='/comment/:rank' component={GetCommentsId} /*The one that works*//>
<Route path='/comment/edit/:testid' component={GetCommentEdit} /*The one I'm having trouble with*//>
<Route path='/comments' component={GetCommentsAll} />
</div>
</BrowserRouter>
);
}
}
The working component:
class GetCommentsId extends Component{
constructor (props) {
super(props)
this.state = {
Comments: [],
output: "",
wasClicked: false,
currentComment: " ",
}
this.handleCommentChange = this.handleCommentChange.bind(this);
}
componentDidMount(){
const { match: { params } } = this.props;
const url = 'http://localhost:51295/api/Values/' + params.rank;
axios.get(url).then(res => {
const comments = res.data;
this.setState({ comments });
this.output = (
<div>
<ul>
{ this.state.comments.map
(
comment =>
(<Comment
QuizId = {comment.Rank}
FirstName = {comment.FirstName}
Comments = {comment.Comments}
TestId = {comment.TestimonialId}
/>)
)}
</ul>
</div>
);
//console.log("From did mount: " + this.currentComment);
this.forceUpdate();
});
}
componentDidUpdate(){}
handleCommentChange(event){
//console.log("handle Comment Change activated");
}
handleClick(comment){
this.wasClicked = true;
this.currentComment = comment.Comments;
console.log(comment.Comments);
this.forceUpdate();
}
render () {
if(this.output != null){
if(!this.wasClicked){
return (this.output);
}
else{
console.log("this is the current comment: " + this.currentComment);
return(
<div>
{this.output}
<NameForm value={this.currentComment}/>
</div>
);
}
}
return ("loading");
}
}
The one that isn't working:
class GetCommentEdit extends Component {
constructor (props) {
super(props)
this.state = {
Comments: [],
output: "",
match: props.match
}
}
componentDidMount(){
const { match: { params } } = this.props;
const url = 'http://localhost:51295/api/Values/' + params.testid;
axios.get(url).then(res => {
const comments = res.data;
this.setState({ comments });
this.output = (
<div>
<ul>
{ this.state.comments.map
(comment =>
(<EditComment
QuizId = {comment.Rank}
FirstName = {comment.FirstName}
Comments = {comment.Comments}
TestId = {comment.TestimonialId}
/>)
)}
</ul>
</div>
);
//console.log("From did mount: " + this.currentComment);
this.forceUpdate();
});
}
render(){
return(
<div>
{this.output}
</div>
);
}
}
I've created a small app for you to demonstrate how to implement working react router v4.
On each route there is a dump of props, as you can see the params are visible there.
In your code I don't see why you are not using Switch from react-router v4, also your routes don't have exact flag/prop. This way you will not render your component views one after another.
Link to sandbox: https://codesandbox.io/s/5y9310y0zn
Please note that it is recommended to wrap withRouter around App component, App component should not contain <BrowserRouter>.
Reviewing your code
Please note that updating state triggers new render of your component.
Instead of using this.forceUpdate() which is not needed here, update your state with values you get from resolving the Promise/axios request.
// Bad implementation of componentDidMount
// Just remove it
this.output = (
<div>
<ul>
{this.state.comments.map
(
comment =>
(<Comment
QuizId={comment.Rank}
FirstName={comment.FirstName}
Comments={comment.Comments}
TestId={comment.TestimonialId}
/>)
)}
</ul>
</div>
);
//console.log("From did mount: " + this.currentComment);
this.forceUpdate();
Move loop function inside render method or any other helper method, here is code for using helper method.
renderComments() {
const { comments } = this.state;
// Just check if we have any comments
// Return message or just return null
if (!comments.length) return <div>No comments</div>;
// Move li inside loop
return (
<ul>
{comments.map(comment => (
<li key={comment.id}>
<Comment yourProps={'yourProps'} />
</li>
))}
</ul>
)
};
Add something like isLoading in your initial state. Toggle isLoading state each time you are done with fetching or you begin to fetch.
this.setState({ isLoading: true }); // or false
// Initial state or implement in constructor
state = { isLoading: true };
Render method will show us loading each time we are loading something, renderComments() will return us comments. We get clean and readable code.
render() {
if (isLoading) {
return <div>Loading...</div>
}
return (
<div>
{this.renderComments()}
</div>
);
}

Categories