When I am passing the store from let store = createStore(myReducer); in props to other component it is coming undefined. this is happening whenI am using react-router-dom but without that it is coming alright.
The route part
the route is in App component
<BrowserRouter>
<div>
<Route path={"/layout"} component={Home} />
<Route path={"/form"} component={UserForm} />
<Route exact path={"/"} component={this.layout} />
</div>
</BrowserRouter
layout method
layout(){
return (
<Layout store={this.props.store} />
);
}
I am passing the store in the app component like this
const renderAll = () => {
console.log("inside render all" ,store);
ReactDOM.render(
<App store={store} />,
document.getElementById('root')
);
}
this store is going to body component from layout component like this
<Body ChangeName={this.handleChangeName} store = { this.store} s_key={this.state.inputkey} />
in body component I am fetching from from a api which is running in the node.js server in the componentWillMount()
fetch('/api/get/all',{
headers : {
'content-Type': 'application/json',
}
}).then((res)=>{
console.log(res);
return res.json();
}).then(users =>{
this.store.dispatch({
type :"FETCH_ALL",
data : users
})
this.setState({
user: users
})
// console.log(this.state.user);
});
I am getting error in the map function
{this.store.getState().user.map(u => {
return <UserDiv name={u.name} age={u.age} location={u.location} gender={u.gender} job={u.job} />;
})}
error
Cannot read property 'map' of undefined
the weird part is when I am doing without the route it is coming alright
thanks in advance
Firstly in order to get the updated state from the store, you need to subscribe to it like
const unsubscribe = store.subscribe(() => {
store.getState();
})
and hence it is not a React way to handle changes.
Secondly, when you are passing store to Layout component, it is entirely possible that you did not bind the layout function and hence this.props.store is undefined.
In order to use Redux the react way, you can make use of Provider and connect methods
import { Provider } from 'react-redux';
const renderAll = () => {
console.log("inside render all" ,store);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
}
and then your Routes can simply be
and you can call Body component from Layout like
<Body ChangeName={this.handleChangeName} s_key={this.state.inputkey} />
and connect the body component like
const mapStateToProps = (state) => {
user: state.user
}
export default connect(mapStateToProps)(Body)
Make sure to use the connected Body component in your Layout component.
After this you can use user state in Body component like
{this.props.user.map(u => {
return <UserDiv name={u.name} age={u.age} location={u.location} gender={u.gender} job={u.job} />;
})}
In case the user value is undefined in the redux store initially, you need to add a check in the Body component before using it like
{this.props.user && this.props.user.map(u => {
return <UserDiv name={u.name} age={u.age} location={u.location} gender={u.gender} job={u.job} />;
})}
Related
I'm using React Router for routing to different routes as below:
<Router>
<Switch>
<Route path="/teams/:teamName/matches" >
<MatchPage/>
</Route>
<Route path="/teams/:teamName" >
<TeamPage/>
</Route>
</Switch>
</Router>
Now in my TeamPage component I'm calling an API using async and then in the render method invoking another component called MatchDetailCard
class TeamPage extends Component {
constructor(props) {
console.log('const called')
super(props)
this.state = {
team: [],
teamName:null
}
}
async componentDidMount() {
console.log(this.props.match.params.teamName);
const teamName = this.props.match.params.teamName;
const response = await fetch(`http://localhost:8080/team/${teamName}`);
const json = await response.json();
console.log(json);
this.setState({team:json, teamName: teamName});
}
componentDidUpdate () {
console.log('updated')
}
render() {
if (!this.state.team || !this.state.team.teamName) {
return <h1>Team not found</h1>;
}
return (
<div className="TeamPage">
<div className="match-detail-section">
<h3>Latest Matches</h3>
<MatchDetailCard teamName={this.state.team.teamName} match={this.state.team.matches[0]}/>
</div>
</div>
);
}
}
export default withRouter(TeamPage);
Within the MatchDetailCard component I create a router Link to the same TeamPage component which will have a different teamName this time as below:
const MatchDetailCard = (props) => {
if (!props.match) return null;
const otherTeam = props.match.team1 === props.teamName ? props.match.team2 : props.match.team1;
const otherTeamRoute = `/teams/${otherTeam}`;
const isMatchWon = props.teamName === props.match.matchWinner;
return (
<div className={isMatchWon ? 'MatchDetailCard won-card' : 'MatchDetailCard lost-card'}>
<div className="">
<span className="vs">vs</span><h1><Link to={otherTeamRoute}>{otherTeam}</Link></h1>
</div>
</div>
);
}
export {MatchDetailCard};
The problem that I'm facing is that the on-click of the link to /team/teamName route only the TeamPage component is not mounting instead it's just getting an update.
I want to have the call to componentDidMount hook to make the API call again in this scenario.
What's the problem with my logic?
If the same component is used as the child of multiple <Route>s at the same point in the component tree, React will see this as the same component instance and the component’s state will be preserved between route changes. If this isn’t desired, a unique key prop added to each route component will cause React to recreate the component instance when the route changes.
https://reactrouter.com/web/api/Route
You can add the teamName as a key prop on the component, which will tell React to unmount/mount the component when the key value changes.
<Router>
<Switch>
<Route
path="/teams/:teamName/matches"
render={({ match }) => {
return <MatchPage key={match.params.teamName} />;
}}
/>
<Route
path="/teams/:teamName"
render={({ match }) => {
return <TeamPage key={match.params.teamName} />;
}}
/>
</Switch>
</Router>
I am trying to conceptualize redux and its working, and after some testing, I have noticed a thing. I would like to quote this example
lets say, I have a single reducer (a boolean variable). based on that variable, the following code happens.
reducer
const initState = { isLoggedIn: false };
const isLoggedInReducer = (state = initState, action) => {
switch (action.type) {
case "LOG_IN":
return { ...state,isLoggedIn: true };
case "LOG_OUT":
return { ...state,isLoggedIn: false };
default:
return state;
}
};
export default isLoggedInReducer;
action
export const logIn = () => {
return {
type:'LOG_IN'
}
}
export const logOut = () => {
return {
type:'LOG_OUT'
}
}
index.js
<Provider store={createStore(isLoggedInReducer)}>
<Router>
<Switch>
<Route path="/" exact>
<AppScreen />
</Route>
<Route path="/auth">
<AuthScreen />
</Route>
<Route path="*">
<NotFoundScreen />
</Route>
</Switch>
</Router>
</Provider>
so, my app firstly directs the user to a component called "mainScreen" , which is as follows
const AppScreen = () => {
let isLoggedIn = useSelector((state) => state.isLoggedIn);
if (isLoggedIn)
return (
<>
<button onClick={() => dispatch(logOut())}>unauthenticate</button>
<NavBar />
<Content />
<BottomBar />
</>
);
else{
return (
<>
<Redirect to="/auth" push />
</>
);
}
};
so if the reducer state has value TRUE , my navbar and stuff is shown, else the user is redirected to the "authScreen" , which is as
const AuthScreen = () => {
let isLoggedIn = useSelector((state) => state.isLoggedIn);
const dispatch = useDispatch();
return isLoggedIn ? (
<>
<Redirect to="/" push />
</>
) : (
<>
<h1> auth is {isLoggedIn?"true":"false"}</h1>
<button onClick={() => dispatch(logIn())}>authenticate</button>
</>
);
};
This creates a setup where "authScreen" can toggle the reducer to TRUE and it re-renders, and finds that reducer is TRUE, so it renders the "mainScreen". Vice versa for "MainScreen"
Now, what components actually re-render ? If I place my authenticate button in the "navbar" instead as a sibling to "navbar" , will it re-render the "navbar" or the "mainScreen" ?
How does redux calculate what component to re-render when a peice of state changes ? How does the useSelector fit in, when I did not even use "connect".
Using hooks with redux made it very confusing. I am sorry if my explanation is hard to understand. The code actually works, I just don't know how.
Any piece of information is appreciated!
Using Redux with a UI always follows the same basic steps:
Render components using initial state
Call store.subscribe() to be notified when actions are dispatched
Call store.getState() to read the latest data
Diff old and new values needed by this component to see if anything actually changed. If not, the component doesn't need to do anything
Update UI with the latest data
React-Redux does that work for you internally.
So, useSelector decides whether a component should re-render based on whatever data you return in your selector functions:
https://redux.js.org/tutorials/fundamentals/part-5-ui-react#reading-state-from-the-store-with-useselector
If the selector return value changes after an action was dispatched, useSelector forces that component to re-render. From there, normal React rendering behavior kicks in, and all child components are re-rendered by default.
Please read my post The History and Implementation of React-Redux and talk A Deep Dive into React-Redux for details on how React-Redux actually implements this behavior.
I build my Apps and learn ReactJS
My code error until now, i try to search on internet and forums but can't fix.
Here my code :
logout(){
localStorage.removeItem("token");
localStorage.clear();
if(localStorage.getItem('token') === null){
this.props.history.push('/login')
}
}
Error : TypeError: Cannot read property 'push' of undefined
Please Help Me
My guess is your logout is within a component which is not a Route it self.
By that what I mean is you're not doing <Route path="/" component={ComponentThatHasLogoutMethod} />
Only the components that are used as Route has history prop.
if you need those in some other nested Component you can use the withRouter Higher order component from react-router-dom
export default withRouter(ComponentThatHasLogoutMethod)
This will pass the history prop to your component and you'll not get null.
Solution Example with react hooks:
import { useHistory } from "react-router-dom";
function HomeButton() {
const history = useHistory();
function handleClick() {
localStorage.removeItem("token")
localStorage.clear()
if(!localStorage.getItem('token')){
history.push("/login")
}
}
return (
<button type="button" onClick={handleClick}>
Login
</button>
);
}
More info you can find in docs
https://reacttraining.com/react-router/web/api/Hooks/usehistory
I have faced similar issue while creating custom portal to show model like logout/login.
const App = () => {
const [modal] = useSelector(({ modal }) => [modal])
return (
<>
<Portal modal={modal} />
<BrowserRouter>
<Routes />
</BrowserRouter>
</>
);
}
export default App;
Correct way-
Portal must me child of BrowserRouter.
const App = () => {
const [modal] = useSelector(({ modal }) => [modal])
return (
<>
<BrowserRouter>
<Portal modal={modal} />
<Routes />
</BrowserRouter>
</>
);
}
For more reference see Router.
We have two solutions for this problem -:
Replace function declaration with fat arrow notation declaration i.e -:
logout = () => {
localStorage.removeItem("token");
localStorage.clear();
if(localStorage.getItem('token') === null){
this.props.history.push('/login');
}
}
We can bind logout function in the constructor -:
constructor(props){
super(props);
this.logout = this.logout.bind(this);
}
I have a react-redux state that is a fetched array of 'products' objects from a backend database. I have a 'ProductList' component that receives the fetched objects and creates a list of products. I am directly calling my 'fetchProducts()' method within this 'ProductList' component. This 'ProductList' component is able to be refreshed and the fetched products will persist and reappear after each refresh.
However, I have another 'ViewProduct' component that is in a nested Router route in which I am also fetching the same 'products' array. The problem is in this component, on refresh, the 'products' state does not persist and returns undefined despite the fact that I am also directly calling 'fetchProducts()' inside the component.
I am passing the products array directly into the 'ProductView' component yet it does not persist on refresh the same way my 'ProductsList' component does.
Here is my 'AllProductsList' component that SUCCESSFULLY persists the products data on refresh. It is located in a route '/products/all':
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { fetchProducts } from '../actions/productAction.js';
import ProductCard from '../components/ProductCard.js';
class AllProductsList extends Component {
componentDidMount() {
this.props.fetchProducts()
}
renderDiv = () => {
return this.props.products.map((product) =>
<ProductCard key={product.id} product={product} />
)
}
render() {
return(
<div>
{this.renderDiv()}
</div>
)
}
}
AllProductsList.propTypes = {
fetchProducts: PropTypes.func.isRequired,
products: PropTypes.array.isRequired,
}
const mapStateToProps = state => ({
products: state.products.items,
})
export default connect(mapStateToProps, { fetchProducts })(AllProductsList)
And here is the problem 'ProductView' component that does NOT persist the product data after refresh. This component is located on route /product/:id :
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { fetchProducts } from '../actions/productAction.js';
class ProductView extends Component {
constructor(props) {
super(props)
this.state = {
buttonMessage: 'Add to cart',
quantity: 0,
}
}
componentDidMount() {
this.props.fetchProducts()
}
quantityChangeReader = (e) => {
this.setState({ quantity: e.target.value })
}
render() {
return(
<div>
<ul>
<li>this.props.products[this.props.match.params.productId].name</li>
<li>this.props.products[this.props.match.params.productId].comments</li>
<li>this.props.products[this.props.match.params.productId].photos[1].url</li>
</ul>
</div>
)
}
}
ProductView.propTypes = {
fetchProducts: PropTypes.func.isRequired,
products: PropTypes.array.isRequired,
}
const mapStateToProps = state => ({
products: state.products.items,
})
export default connect(mapStateToProps, { fetchProducts })(ProductView)
Parent component of 'ProductView', which is routed to in the App.js:
import React from 'react';
import ProductView from '../components/ProductView.js';
import { Route } from 'react-router-dom';
const ProductViewContainer = ({ match, products }) => (
<div>
<Route path={`${match.url}/:productId`} render={routerProps => <ProductView {...routerProps} /> }/>
</div>
)
export default ProductViewContainer
In this second scenario, for component ProductView, on refresh, it will return the error message, 'Cannot read .name of undefined.' Yet in the first component ('AllProductsList') refreshing will successfully return the array again.
As requested, here is the router:
<Provider store={store}>
<Router>
<NavBar />
<Route exact path="/" component={Carousel} />
<Route exact path="/about" component={OurStory} />
<Route exact path="/products/necklaces" component={NecklacesList} />
<Route exact path="/products/bracelets" component={BraceletsList} />
<Route exact path="/products/earrings" component={EarringsList} />
<Route exact path="/products/all" component={AllProductsList} />
<Route exact path="/events" component={Events} />
<Route exact path="/cart" component={Cart} />
<Route path='/product' render={routerProps => <ProductViewContainer {...routerProps} />} />
<Credits />
</Router>
</Provider>
In the ProductView component, this.props.products will be undefined when the component renders for the first time. This will happen on page refresh or when you hit the product id route directly in the browser, either of which will reset your redux state.
To curb this, you'll need to add a check in your component to confirm if the products prop has been populated.
Like this...
render() {
const { products } = this.props;
if (products.length) {
// render the rest of your view
} else {
// products is still fetching...
}
}
React-redux does not persist data when you refresh page. If you want to persist data, store in localstorage.
When you refresh page, initially this.props.products will be null. Once data get fetch, this.props.products will have remote data, and component will get re-render. You can put if condition in ProductView component
Here is my major code, App component is connected to Redux's store:
class App extends Component {
render() {
const { requestQuantity } = this.props;
return (
<div>
<Router>
<Switch>
<Route exact path="/" component={PostList} />
<Route path="/login" component={Login} />
<Route path="/topics" component={PostList} />
</Switch>
</Router>
{requestQuantity > 0 && <Loading />}
</div>
);
}
}
const mapStateToProps = (state, props) => {
return {
requestQuantity: getRequestQuantity(state)
};
};
export default connect(mapStateToProps)(App);
PostList component is also connected to Redux's store:
class PostList extends Component {
componentDidMount() {
this.props.fetchAllPosts();
}
render() {
const { posts} = this.props;
return (
// ...
);
}
//...
}
const mapStateToProps = (state, props) => {
return {
posts: getPostList(state),
};
};
const mapDispatchToProps = dispatch => {
return {
...bindActionCreators(postActions, dispatch),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(PostList);
When this.props.fetchAllPosts() is called, the requestQuantity in the global state will change from 0 to 1 (request starts) then to 0 (request ends). So the App will re-render twice. However, every re-rendering of App also causes PostList to re-render, which is what I don't expect, since PostList only depends on posts in the global state and posts doesn't change in these twice re-rendering.
I check React Router's source code and find the Route's componentWillReceiveProps will always call the setState, which set a new match object:
componentWillReceiveProps(nextProps, nextContext) {
warning(
!(nextProps.location && !this.props.location),
'<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
)
warning(
!(!nextProps.location && this.props.location),
'<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
)
//the official always set a new match object ignoring whether the nextProps change or not
this.setState({
match: this.computeMatch(nextProps, nextContext.router)
})
}
It is the new match prop passed to the PostList causing the Redux's shallow comparison fails and re-rendering occurs. I hope React Router's team can do some easy logic before setState, such as using (===) comparing every prop in nextProps and this.props, if no change occurs, skip setState. Unfortunately,they think it is not a big deal and closed my issue.
Now my solution is creating a HOC :
// connectRoute.js
export default function connectRoute(WrappedComponent) {
return class extends React.Component {
shouldComponentUpdate(nextProps) {
return nextProps.location !== this.props.location;
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
Then use connectRoute to wrap the containers used in Route:
const PostListWrapper = connectRoute(PostList);
const LoginWrapper = connectRoute(Login);
class App extends Component {
render() {
const { requestQuantity } = this.props;
return (
<div>
<Router>
<Switch>
<Route exact path="/" component={PostListWrapper} />
<Route path="/login" component={LoginWrapper} />
<Route path="/topics" component={PostListWrapper} />
</Switch>
</Router>
{requestQuantity > 0 && <Loading />}
</div>
);
}
}
Besides, when React Router is used with Mobx, this issue is also easy to meet.
Hope someone could offer better solutions. A long question. Thanks for your patience.