Component Re-renders after clicking NavLink & loads same data Multiple times - reactjs

I have made a sample e-commerce site for practice & I used React, Redux & Router-DOM. For the first time, everything loads perfectly. it has two routes ScreenShot-Link. Home route takes me to the Home page where the problem occurs & the Upload route takes me to the upload page. But when I again go back to HomeRoute the data which was fetched from an API doesn't re-render instead the state gets doubled I mean the existing state remains unchanged but for some reason, the Data gets fetched again and the elements get rendered for 2 times ScreenShot-2-Link.
Body Element which gets doubled if I route back to it from another route...
const Body = ({getProducts, deleteProduct, productState})=>{
const {products, loading, error} = productState;
useEffect(()=>{
getProducts()
}, [getProducts])
const deleteProducts = (id)=>{
deleteProduct(id)
}
return (
<div className="display-flex width-full flex-wrap justify-content-center">
{!loading && !error && products?products.map(product=>(
product.data?product.data.map(d=>(
<BodyDiv
key={uuid.v4()}
imgSrc={d.imgSrc}
title={d.title}
details={d.details}
description={d.description}
onClick={()=>deleteProducts(d._id)}
/>
)):product.map(d=>(
<BodyDiv
key={uuid.v4()}
imgSrc={d.imgSrc}
title={d.title}
details={d.details}
description={d.description}
onClick={()=>deleteProducts(d._id)}
/>
))
))
: loading&&!error
? <div className="vertical-center-strict horizontal-center-strict">Loading.......</div>
: <div className="vertical-center-strict horizontal-center-strict">No Internet!</div>
}
</div>
)
}
const BodyDiv = (props)=>{
return(
<div className="container-div width-full card tiny-padding align-self-start">
<img title={props.title} className="img-responsive hover-filter" src={props.imgSrc}/>
<h2 className="text-align-center">{props.title}</h2>
<h5>{props.details}</h5>
<p className="text-align-justify">{props.description}</p>
<button
className="btn btn-danger"
onClick={props.onClick}
>Delete</button>
</div>
)
}
BodyDiv.propTypes = {
title: PropTypes.string.isRequired,
img: PropTypes.string,
details: PropTypes.string.isRequired,
description: PropTypes.string,
onClick: PropTypes.func
}
const mapStateToProps = (state)=>({
productState: state.productState
})
const conn = connect(mapStateToProps, {getProducts, deleteProduct})(Body)
export default conn;
Router Component & Nav Component
//Parent Router Comonent
const App = () => {
return (
<Router>
<AppNavBar />
<Switch>
<Route path="/" exact>
<Body />
</Route>
<Route path="/upload">
<Upload />
</Route>
<Route path="*">
<div className="vertical-center-strict top-20">
<h1 className="text-align-center">404</h1>
<h3>Page doesn't exist</h3>
<p>Please give the correct address!</p>
</div>
</Route>
</Switch>
</Router>
);
};
export default App;
//NavLink Components..
const NavBar = ()=>{
return(
<>
<NavLink activeClassName="link" to="/" exact>
<NavItem content="Home" /> //<a> element with content as a prop
<NavLink>
<NavLink activeClassName="link" to="/upload">
<NavItem content="Upload"
</NavLink>
</>
)
}
}
My Redux Code: Store & Reducers
//Store
import {createStore, applyMiddleware} from "redux"
import rootReducer from "./reducers/index"
import thunk from "redux-thunk"
const middleWare = [thunk]
const store = createStore(rootReducer, applyMiddleware(...middleWare))
export default store
//RootReducer
import {
FETCHED_PRODUCTS,
FETCHING_PRODUCTS,
ERROR_GET_PRODUCTS,
DELETED_PRODUCT,
ERROR_DELETING_PRODUCT
} from "../actions/productActions";
const initialState = {
products: [],
loading: true,
error: false,
};
const productReducer = (state = initialState, action) => {
switch (action.type) {
case FETCHING_PRODUCTS:
return {
...state,
loading: true,
};
case FETCHED_PRODUCTS:
return {
...state,
products: state.products.concat(action.payload),
loading: false,
error: false,
};
case ERROR_GET_PRODUCTS:
return {
...state,
loading: false,
};
case DELETED_PRODUCT:
return {
...state,
products: state.products.map((product) =>
product.data.filter((d) => d._id !== action.payload)
),
error: false,
};
case ERROR_DELETING_PRODUCT:
return {
...state,
error: true,
};
default:
return state;
}
};
export default productReducer;
ActionCreators
export const getProducts = (payload)=>(dispatch)=>{
return(
fetch("/api/products")
.then(res=>{
dispatch({
type:FETCHING_PRODUCTS
})
if(!res.ok){
dispatch({
type: ERROR_GET_PRODUCTS
})
}
return res.json();
})
.then(json=>{
if(json)dispatch({
type: FETCHED_PRODUCTS,
payload: json
})
})
.catch(err=>{
console.log("Error!! failed to fetch data: "+ err)
})
)
}
export const deleteProduct = (payload)=>dispatch=>{
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
var requestOptions = {
method: 'DELETE',
headers: myHeaders,
body: JSON.stringify({"_id": payload}),
redirect: 'follow'
};
fetch("/api/product-delete", requestOptions).then(res=>{
dispatch({
type: DELETED_PRODUCT,
payload
})
if(res.status === 501 || res.status === 403){
dispatch({
type: ERROR_DELETING_PRODUCT,
})
}
})
.catch(err=>{
console.log("Failed to Delete")
})
}
Please help me I have been searching for the solution for 4 hours...It would be great if you help me...Thanks in advance

When using the useEffect hook the second parameter are dependencies.. so it calls the function again if the dependencies change. Now since getProductsis a function, it will never get called again. Instead, you should put products in there
useEffect(()=>{
getProducts()
}, [products])
//will refetch data everytime product state is updated.
It is a little bit hard to see, what is going on in the code. But I suspect maybe this is the problem. Otherwise let me know.

Related

Make react-router-dom v6 pass path as key to rendered element

I think I may need a paradigm shift in my thinking here, so I'm open to those kinds of answers as well here.
Consider the following simplified example:
export const App = (p: { apiClient: SomeApiClient }) => {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/posts/:postId" element={<Post apiClient={p.apiClient} />} />
</Routes>
);
}
export const Home = () => {
return <h1>Home!</h1>
}
export const Post = (p: { apiClient: SomeApiClient }) => {
const { postId } = useParams();
const [ state, setState ] = useState<PostState>({ status: "loading" });
// When the component mounts, get the specified post from the API
useEffect(() => {
if (state.status === "loading") {
(async () => {
const post = await p.apiClient.getPost(postId);
setState({ status: "ready", post });
})();
}
})
return (
<h2>Posts</h2>
{
state.status === "loading"
? <p>Loading....</p>
: <div className="post">
<h3>{state.post.title}</h3>
<div className="content">{state.post.content}</div>
</div>
}
)
}
export type PostState =
| { status: "loading" }
| { status: "ready"; post: BlogPost };
export type BlogPost = { title: string; content: string };
This works fine the first time, but pretend there's a <Link /> on the page that goes to the next post. When I click that link, the URL changes, but the page content doesn't, because React Router is not actually re-mounting the <Post .../> component. That component correctly receives the updated postId and is re-rendered, but since it doesn't get re-mounted, the useEffect logic doesn't run again and the content stays the same.
I've been solving this very awkwardly by creating intermediary components like so:
export const App = (p: { apiClient: SomeApiClient }) => {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/posts/:postId" element={<PostRenderer apiClient={p.apiClient} />} />
</Routes>
);
}
export const PostRenderer = (p: { apiClient: SomeApiClient }) => {
const { postId } = useParams();
return <Post key={postId} postId={postId} apiClient={p.apiClient} />
}
export const Post = (p: { postId: string; apiClient: SomeApiClient }) => {
// ....
}
But I'm starting to get a lot of those, and literally all they do is take the param from the URL and use it as a key on the actual target component. I've read through the react-router-dom docs and am not finding anything that indicates there's a way to automate this. I must be thinking about this wrong.... Any suggestions are appreciated.
I think a more common and practical solution is to add the postId as a dependency to the useEffect to rerun the asynchronous logic when the route param changes.
Example:
export const Post = (p: { apiClient: SomeApiClient }) => {
const { postId } = useParams();
const [state, setState] = useState<PostState>({ status: "loading" });
// When the post id updates, get the specified post from the API
useEffect(() => {
const fetchPostById = async (postId) => {
setState({ status: "loading" });
const post = await p.apiClient.getPost(postId);
setState({ status: "ready", post });
};
fetchPostById(postId);
}, [postId]);
return (
<h2>Posts</h2>
{
state.status === "loading"
? <p>Loading....</p>
: <div className="post">
<h3>{state.post.title}</h3>
<div className="content">{state.post.content}</div>
</div>
}
)
};

Am I awaiting incorrectly or is there another issue?

I'm new to React, but not development. I'm in my first independent project since training. I'm using redux.
I've created a simple list app that requires login. Login is working. BUT. If I login, then refresh, the redux store loses everything. So I'm setting the user's token in session storage to overcome this issue, so that I can get the user's data from the database with the token if they refresh.
The problem seems to be that when I use axios to get the user's data from the database based on the token, it's not awaiting properly and then continues to process the page and bombs because it doesn't have the user_id yet. It's bombing because I'm trying to access user_id from this.props.auth.userId.user_id and it doesn't have it for some reason.
App.js
import React from 'react';
import { Router, Route, Switch } from 'react-router-dom';
import { connect } from 'react-redux';
import ListItemCreate from './listitems/listItemCreate';
import ListItemEdit from './listitems/listItemEdit';
import ListItemDelete from './listitems/listItemDelete';
import ListItemList from './listitems/listItemList';
import ListItemShow from './listitems/listItemShow';
import Header from './Header';
import history from '../history';
import Login from './Login/Login';
import Register from './Login/Register';
import { signInWithToken } from '../actions';
const setToken = (userToken) => {
sessionStorage.setItem('token', JSON.stringify(userToken));
}
const getToken = () => {
//sessionStorage.removeItem('token');
const tokenString = sessionStorage.getItem('token');
if(tokenString){
const userToken = JSON.parse(tokenString);
console.log('The Token for you, madam: ' +userToken.token);
return userToken?.token;
}
return null;
}
class App extends React.Component {
//state = { isSignedIn: null };
render(){
const isSignedIn = this.props.auth.isSignedIn;
const curToken = getToken();
console.log('curToken: '+curToken);
if(curToken && !isSignedIn){
console.log('need signInWithToken');
this.props.signInWithToken(curToken);
}
if(isSignedIn){
console.log('because isSignedIn is TRUE');
//console.log(this.props.auth);
if(!curToken){setToken({ 'token': this.props.auth.userId.token });}
const user_id = this.props.auth.userId.user_id;
const email = this.props.auth.userId.email;
const token = this.props.auth.userId.token;
console.log('isSignedId:' + isSignedIn);
console.log('user_id:'+user_id);
console.log('email:' + email);
console.log('token:' + token);
console.log('getting isSigned in');
if (isSignedIn) {
console.log('it has VALUE');
console.log(this.props.isSignedIn);
} else {
console.log('no isSignedIn');
}
}
if(curToken){console.log('curToken:' + curToken);}
//const token = getToken();
//console.log();
if(isSignedIn || curToken){
console.log('i HAVE a token');
return(
<div className="ui container">
<Router history={history}>
<div>
<Header />
<Switch>
<Route path="/" exact component={ListItemList} />
<Route path="/listitems/new" exact component={ListItemCreate} />
<Route path="/listitems/edit/:list_item_id" exact component={ListItemEdit} />
<Route path="/listitems/delete/:list_item_id" exact component={ListItemDelete} />
<Route path="/listitems/:list_item_id" exact component={ListItemShow} />
</Switch>
</div>
</Router>
</div>
);
}else{
console.log('NO token');
return(
<div className="ui container">
<Router history={history}>
<div>
<Header />
<Switch>
<Route path="/" exact component={Login} setToken={setToken} />
<Route path="/register" exact component={Register} setToken={setToken} />
</Switch>
</div>
</Router>
</div>
);
}
}
}
const mapStateToProps = (state) => {
return { auth: state.auth };
};
export default connect(mapStateToProps, { signInWithToken })(App);
From the action creator (but not the whole file):
export const signInWithToken = (token) => async (dispatch) => {
console.log('signInWithToken');
let data = new FormData();
data.append('grant_type', 'signinwithtoken');
data.append('token', token);
const response = await user.post(`theurl/token`, data)
.then(response => {
console.log('and the response is'); // << It's showing me this in the console just AFTER the error where it needs to access the user_id
console.log(response.data);
dispatch({ type: SIGN_IN_WITH_TOKEN, payload: response.data });
history.push('/');
return {
type: SIGN_IN_WITH_TOKEN,
payload: response.data
};
});
};
The Reducer:
import { SIGN_IN, SIGN_OUT, LOGIN, REGISTER, SIGN_IN_WITH_TOKEN } from '../actions/types';
const INITIAL_STATE = {
isSignedIn: null,
userId: null
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case REGISTER:
return { ...state, isSignedIn: true, userId: action.payload };
case LOGIN:
return { ...state, isSignedIn: true, userId: action.payload };
case SIGN_IN:
return { ...state, isSignedIn: true, userId: action.payload };
case SIGN_OUT:
return { ...state, isSignedIn: false, userId: null };
case SIGN_IN_WITH_TOKEN:
return { ...state, isSignedIn: true, userId: action.payload };
default:
return state;
}
};
Here is ListItemList that is rendered as long as there's a user_id:
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { fetchListItems, markListItemCompleted } from '../../actions';
import MultiSort from '../MultiSort';
class ListItemList extends React.Component {
componentDidMount(){
console.log('in ListItemList');
console.log(this.props);
const id = this.props.currentUserId;
if(id){
this.props.fetchListItems(id);
}else{
}
}
markCompleted(listItem){
console.log('in markCompleted');
console.log(listItem);
// you need to pass the data, but completion datetime needs to be set
let currentdate = new Date();
let datetime = currentdate.getDate() + "/"
+ (currentdate.getMonth()+1) + "/"
+ currentdate.getFullYear() + " "
+ currentdate.getHours() + ":"
+ currentdate.getMinutes() + ":"
+ currentdate.getSeconds();
console.log(datetime);
this.props.markListItemCompleted(listItem.list_item_id);
}
renderAdmin(listItem){
// if(listItem.userId === this.props.currentUserId){
return (
<div className="right floated content">
<button className="ui button secondary" onClick={() => this.markCompleted(listItem)} >Mark {listItem.completion_label}</button>
<Link to={`/listItems/edit/${listItem.list_item_id}`} className="ui button primary">Edit</Link>
<Link to={`/listItems/delete/${listItem.list_item_id}`} className="ui button negative">Delete</Link>
</div>
);
//}
}
formatCompletedDT(dt = null){
if(dt === "0000-00-00 00:00:00"){return "Never";}
let date = new Date(dt);
let day = date.getDate();
let month = date.getMonth() + 1;
let year = date.getFullYear();
let hour = date.getHours();
let minute = date.getMinutes();
let second = date.getSeconds();
return month + '/' + day + '/' + year + ' ' + hour + ':' + minute + ':' + second;
}
renderList(){
const noNulls = this.props.listItems.map(listItem => {
if(!listItem.last_completed_dt){
listItem.last_completed_dt = "0000-00-00 00:00:00";
}
return listItem;
});
const sortedList = MultiSort(noNulls, { last_completed_dt: 'asc', list_item: 'asc'});
return sortedList.map(listItem => {
const lastCompleted = this.formatCompletedDT(listItem.last_completed_dt);
// if(listItem.userId === this.props.currentUserId){
return (
<div className="item" key={listItem.list_item_id}>
{this.renderAdmin(listItem)}
<i className="large middle aligned icon check circle" />
<div className="content">
<Link to={`/listItems/${listItem.list_item_id}`} className="header">
{listItem.list_item}
</Link>
<div className="description" style={{ color: 'grey', fontSize: '9pt' }} >Last {listItem.completion_label}: {lastCompleted}</div>
</div>
</div>
);
// }
});
}
renderCreateButton(){
//if(this.props.isSignedIn){
return(
<div style={{ textAlign: 'right' }}>
<Link to="/listItems/new" className="ui button primary">
Create List Item
</Link>
</div>
);
//}
}
render(){
return (
<div>
<h2>List Items</h2>
<div className="ui celled list">{this.renderList()}</div>
{this.renderCreateButton()}
</div>
);
}
}
const mapStateToProps = (state) => {
return {
listItems: Object.values(state.listItems),
currentUserData: state.auth.userId,
isSignedIn: state.auth.isSignedIn,
auth: state.auth
};
};
export default connect(mapStateToProps, { fetchListItems, markListItemCompleted })(ListItemList);
And the action creator for ListItemList:
export const fetchListItems = (userId) => async dispatch => {
const response = await listItems.get(`/listItemsMaintenance?user_id=${userId}`);
dispatch({ type: FETCH_LIST_ITEMS, payload: response.data });
};
I trust that if my plan is horrendously flawed, you'll tell me.
Your problem is this if statement: if(isSignedIn || curToken){ when it renders with token but not yet signed in then you try to render as if you already have the user information. Implement setting a loading value in your reducer and set it to true in your signInWithToken action before doing async and set it to false when the promise is done (fail or success).

Reactjs props keep getting re-rendered

I am pretty newbie to react and its lifecycles, I am trying to get a specific profile data that created a specific post. I am able to receive data from the profile props but it has a strange behavior.
So when I console.log(profile) in render method, profile stays null along with rendering post list, then I get the value from the props, then it goes back to null again and eventually I am able to see the profile that exists. But during that loading, spinner and data blinks based on profile props.
Is it a good a idea to initialize a state with profile props? Or how could remove this behavior?
class PostItem extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.getProfileByUserId(this.props.post.user);
}
render() {
const { post } = this.props;
let username = ''
const { profile,loading } = this.props.profile;
if (!profile ) {
username = (
<span className={styles['username-style']}> {post.name + " "}</span>)
} else {
username = (
<Link to={`/profile/${post.user}`} className={styles["profile-link"]}>
{post.name + " "}
</Link>)
}
let postItemListView = "";
if (loading) {
postItemListView = (
<Grid>
<Grid.Column>
<Segment raised>
<Placeholder>
<Placeholder.Header image>
<Placeholder.Line />
<Placeholder.Line />
</Placeholder.Header>
<Placeholder.Paragraph>
<Placeholder.Line length="medium" />
<Placeholder.Line length="short" />
</Placeholder.Paragraph>
</Placeholder>
</Segment>
</Grid.Column>
</Grid>
);
} else {
postItemListView = (
<Link to={`/post/${post._id}`}>
<div className={styles["link-wrapper"]}>
<Grid>
<Grid.Column width={16}>
<Grid.Row style={{ padding: "10px 0" }}>
<h4 className={styles["subject-style"]}>
{post.subject.charAt(0).toUpperCase() +
post.subject.slice(1)}
</h4>
</Grid.Row>
<Grid.Row style={{ padding: "10px 0" }}>
<div className={styles["body-style"]}>{post.text}</div>
</Grid.Row>
<Grid.Row>
<div className={styles["detailed-text__style"]}>
{username}
created {moment(post.createdAt).fromNow()}
</div>
</Grid.Row>
</Grid.Column>
</Grid>
</div>
</Link>
);
}
return <div>{postItemListView}</div>;
}
}
PostItem.propTypes = {
post: PropTypes.object.isRequired,
profile: PropTypes.object.isRequired,
getProfileByUserId: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
auth: state.auth,
profile: state.profile
});
export default connect(
mapStateToProps,
{ getProfileByUserId }
)(React.memo(PostItem));
EDIT:
This how API requests happen in redux:
export const getProfiles = () => dispatch => {
dispatch(setProfileLoading());
axios
.get(`${PROFILE_API_URL}/all`)
.then(res =>
dispatch({
type: GET_PROFILES,
payload: res.data
})
)
.catch(err =>
dispatch({
type: GET_PROFILES,
payload: null
})
);
}
And this is the reducer for profile:
import {
GET_PROFILE,
GET_PROFILES,
PROFILE_LOADING,
CLEAR_CURRENT_PROFILE
} from 'actions/types';
const initialState = {
profile: {},
profiles: [],
loading: false
};
export default function(state = initialState, action) {
switch (action.type) {
case PROFILE_LOADING:
return {
...state,
loading: true
};
case GET_PROFILE:
return {
...state,
profile: action.payload,
loading: false
};
case GET_PROFILES:
return {
...state,
profiles: action.payload,
loading: false
};
case CLEAR_CURRENT_PROFILE:
return {
...state,
profile: null
};
default:
return state;
}
}
ok I'm not entirely sure of the problem in your app but I think you should do something like this
class PostItem extends Component {
constructor(props) {
super(props);
this.state = {
profile: null;
loading: true;
}
}
async componentDidMount() {
// make api request here and make it async
await const result = this.props.getProfileByUserId(this.props.post.user);
this.setState({profile: result, loading: false})
}
render() loading? (
<div> Loading... </div>
) : (
<div>
<Profile />
</div>
)
}
slight pseudo code but let me know if that helps?

How to add page number to the URL

Could someone please tell me how can I add page number to my url. The component is as follows:
/** NPM Packages */
import React, { Component } from "react";
import { connect } from "react-redux";
import { Spinner, Pagination } from "react-bootstrap";
//import styles from "./App.module.css";
/** Custom Packages */
import List from "../List";
//import fetchCategories from "../../../actions/configuration/category/fetchCategories";
import deleteCategory from "../../../actions/configuration/category/deleteCategory";
import API from "../../../../app/pages/utils/api";
class Category extends Component {
constructor(props) {
super(props);
this.state = {
mesg: "",
mesgType: "",
isLoading: true,
total: null,
per_page: null,
current_page: 1,
pdata: []
};
this.fetchCategoriesAPI = this.fetchCategoriesAPI.bind(this);
}
fetchCategoriesAPI = async pno => {
await API.get("categories?offset=" + (pno.index+1))
.then(res => this.setState({ pdata: res.data }))
.then(() => this.props.passToRedux(this.state.pdata))
.catch(err => console.log(err));
};
componentDidMount = async () => {
const { state } = this.props.location;
if (state && state.mesg) {
this.setState({
mesg: this.props.location.state.mesg,
mesgType: this.props.location.state.mesgType
});
const stateCopy = { ...state };
delete stateCopy.mesg;
this.props.history.replace({ state: stateCopy });
}
this.closeMesg();
await this.fetchCategoriesAPI(1);
this.setState({ isLoading: false });
};
onDelete = async id => {
this.props.removeCategory(id);
await deleteCategory(id).then(data =>
this.setState({ mesg: data.msg, mesgType: "success" })
);
this.closeMesg();
};
closeMesg = () =>
setTimeout(
function() {
this.setState({ mesg: "", mesgType: "" });
}.bind(this),
10000
);
/** Rendering the Template */
render() {
let activePage = this.state.pdata.currPage;
let items = [];
let totalPages = Math.ceil(this.state.pdata.totalCount / 10);
for (let number = 1; number <= totalPages; number++) {
items.push(
<Pagination.Item key={number} active={number == activePage}>
{number}
</Pagination.Item>
);
}
const paginationBasic = (
<div>
<Pagination>
{items.map((item,index)=>{
return <p key={index} onClick={() => this.fetchCategoriesAPI({index})}>{item}</p>
})}
</Pagination>
<br />
</div>
);
const { mesg, mesgType, isLoading } = this.state;
return (
<>
{mesg ? (
<div
className={"alert alert-" + mesgType + " text-white mb-3"}
role="alert"
>
{mesg}
</div>
) : (
""
)}
{isLoading ? (
<div className="container-fluid">
<h4
className="panel-body"
style={{ "text-align": "center", margin: "auto" }}
>
Loading
<Spinner animation="border" role="status" />
</h4>
</div>
) : (
<div>
<List
listData={this.props.categories}
listName="category"
_handleDelete={this.onDelete.bind(this)}
/>
{paginationBasic}
</div>
)}
</>
);
}
}
const matchStatestoProps = state => {
return { categories: state.categories };
};
const dispatchStatestoProps = dispatch => {
return {
passToRedux: pload =>
dispatch({ type: "FETCH_CATEGORIES", payload: pload }),
removeCategory: id => dispatch({ type: "DELETE_CATEGORY", payload: id })
};
};
export default connect(matchStatestoProps, dispatchStatestoProps)(Category);
the route is as follows:
<Route exact path="/categories/:page?" component={Category} />
So basically I want the page number to be displayed in the URL. Also if I change the page number, the data should load the corresponding page. Please help me
Could someone please help me out?
In a class component:
Your router will pass match in as a prop. When your component mounts, get this.props.match.params.page and load the data accordingly:
class MyComponent extends React.Component {
componentDidMount () {
// get the 'page' param out of the router props.
// default to 0 if not specified.
const { page = 0 } = this.props.match.params;
// it comes in as a string, parse to int
const p = parseInt(page, 10);
// do whatever you need to do (load data, etc.)
}
}
In a function component:
In a function component, you can get the page param via react-router's useParams hook:
import { useParams } from 'react-router-dom';
function MyComponent () {
const { page } = useParams(); // get the 'page' router param
const p = parseInt(page, 10); // comes in as a string, convert to int
// do whatever you need to do with it
}
If you need prev/next navigation you can deduce those page numbers from the current page.
I made this quick example that demonstrates how to access and use the route's url parameters via react router's useParams hook and how to do it via the match prop with a class component.
You can get page number from props like this:
const matchStatestoProps = (state, ownProps) => {
return { id: ownProps.match.params.id; categories: state.categories };
};
In your routes:
<Route path="/page/:id" component={Page} />

Custom react hook triggers api call multiple times

I cannot figure out how to handle my function components calling my api repeatedly. I have two components which retrieve data, one of them calls the api twice. Once before the second component once after.
I am using a custom react hook and axios get method to retrieve the data. My two components are are nested. The first component when loads and fetches data. Inside this component is a child component which when renders it fetches data right before passing the first set of data as props to another child component. When it completes loading it reloads the first child component which again calls the api for data. I understand the function components reload on state change. I would be happy for it to not call the api a second time. Is there a way to check if it already has data and bypass the api call?
Custom hook to retrieve data
import React, { useState, useEffect, useReducer } from "react";
import axios from "axios";
const dataFetchReducer = (state, action) => {
switch (action.type) {
case "FETCH_INIT":
return { ...state, isLoading: true, hasErrored: false };
case "FETCH_SUCCESS":
return {
...state,
isLoading: false,
hasErrored: false,
errorMessage: "",
data: action.payload
};
case "FETCH_FAILURE":
return {
...state,
isLoading: false,
hasErrored: true,
errorMessage: "Data Retrieve Failure"
};
case "REPLACE_DATA":
// The record passed (state.data) must have the attribute "id"
const newData = state.data.map(rec => {
return rec.id === action.replacerecord.id ? action.replacerecord : rec;
});
return {
...state,
isLoading: false,
hasErrored: false,
errorMessage: "",
data: newData
};
default:
throw new Error();
}
};
const useAxiosFetch = (initialUrl, initialData) => {
const [url] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
hasErrored: false,
errorMessage: "",
data: initialData
});
useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: "FETCH_INIT" });
try {
let result = await axios.get(url);
if (!didCancel) {
dispatch({ type: "FETCH_SUCCESS", payload: result.data });
}
} catch (err) {
if (!didCancel) {
dispatch({ type: "FETCH_FAILURE" });
}
}
};
fetchData();
return () => {
didCancel = true;
};
}, [url]);
const updateDataRecord = record => {
dispatch({
type: "REPLACE_DATA",
replacerecord: record
});
};
return { ...state, updateDataRecord };
};
export default useAxiosFetch;
Main component which renders the "CompaniesDropdown" twice inside
CompaniesDropdown is one of three dropdowns within the ListFilterContainer component but the only one which calls the api more than once. The other two dropdowns load by selection of the CompaniesDropdown.
import React, { useMemo, useEffect, useContext } from "react";
import InvoiceList from "../src/Components/Lists/InvoiceList";
import useAxiosFetch from "../src/useAxiosFetch";
import { ConfigContext } from "./_app";
import ListFilterContainer from "../src/Components/Filters/InvoiceFilters";
// import "../css/ListView.css";
const Invoices = props => {
const context = useContext(ConfigContext);
useEffect(() => {
document.title = "Captive Billing :: Invoices";
});
const {
data,
isLoading,
hasErrored,
errorMessage,
updateDataRecord
} = useAxiosFetch("https://localhost:44394/Invoice/GetInvoices/false", []);
const newInvoicesList = useMemo(
() => data
// .filter(
// ({ sat, sun }) => (speakingSaturday && sat) || (speakingSunday && sun)
// )
// .sort(function(a, b) {
// if (a.firstName < b.firstName) {
// return -1;
// }
// if (a.firstName > b.firstName) {
// return 1;
// }
// return 0;
// }),
// [speakingSaturday, speakingSunday, data]
);
const invoices = isLoading ? [] : newInvoicesList;
if (hasErrored)
return (
<div>
{errorMessage} "Make sure you have launched "npm run json-server"
</div>
);
if (isLoading) return <div>Loading...</div>;
const dataProps = {
data: invoices,
titlefield: "invoiceNumber",
titleHeader: "Invoice Number:",
childPathRoot: "invoiceDetail",
childIdField: "invoiceId",
childDataCollection: "invoiceData"
};
var divStyle = {
height: context.windowHeight - 100 + "px"
};
return (
<main>
<ListFilterContainer />
<section style={divStyle} id="invoices" className="card-container">
<InvoiceList data={dataProps} />
</section>
</main>
);
};
Invoices.getInitialProps = async ({ req }) => {
const isServer = !!req;
return { isServer };
};
export default Invoices;
Actual result is described above. My main concern is to not have the api calls more than once.
Here is some additional code to help. It is the filter control mentioned above. It, as you will notice really just contains dropdowns and a text box. The first dropdown is the one that calls the api twice. The second two are not visible until that one is selected.
import React, { useState, useMemo } from "react";
import CompaniesDropdown from "../Dropdowns/CompaniesDropdown";
import LocationsDropdown from "../Dropdowns/LocationsDropdown";
import AccountsDropdown from "../Dropdowns/AccountsDropdown";
import Search from "./SearchFilter/SearchFilter";
const InvoiceFilters = props => {
const [company, setCompany] = useState("");
const [location, setLocation] = useState(undefined);
const [account, setAccount] = useState(undefined);
const handleClientChange = clientValue => {
setCompany(clientValue);
};
const handleLocationsChange = locationValue => {
setLocation(locationValue);
};
const handleAccountsChange = AccountValue => {
setAccount(AccountValue);
};
return (
<section className="filter-container mb-3">
<div className="form-row">
<div className="col-auto">
<CompaniesDropdown change={e => handleClientChange(e)} />
</div>
<div className="col-auto">
<LocationsDropdown
selectedCompany={company}
change={e => handleLocationsChange(e)}
/>
</div>
<div className="col-auto">
<AccountsDropdown
selectedCompany={company}
change={e => handleAccountsChange(e)}
/>
</div>
<div className="col-auto">
<Search />
</div>
</div>
</section>
);
};
InvoiceFilters.getInitialProps = async ({ req }) => {
const isServer = !!req;
return { isServer };
};
export default InvoiceFilters;
Also the datalist
import React from "react";
import Link from "next/link";
import InvoiceListRecord from "./InvoiceListRecord";
const InvoiceList = props => {
let dataCollection = props.data.data;
return dataCollection.length == 0 ? "" : dataCollection.map((item, index) => {
return (
<section key={"item-" + index} className="card text-left mb-3">
<header className="card-header">
<span className="pr-1">{props.data.titleHeader}</span>
<Link
href={
"/" +
props.data.childPathRoot +
"?invoiceId=" +
item[props.data.childIdField]
}
as={
"/" +
props.data.childPathRoot +
"/" +
item[props.data.childIdField]
}
>
<a>{item[props.data.titlefield]}</a>
</Link>{" "}
</header>
<div className="card-body">
<div className="row">
<InvoiceListRecord
data={item}
childDataCollection={props.data.childDataCollection}
/>
</div>
</div>
</section>
);
});
};
InvoiceList.getInitialProps = async ({ req }) => {
console.log("Get Intitial Props works: Invoices Page!");
const isServer = !!req;
return { isServer };
};
export default InvoiceList;
and the list items component.
import React from "react";
const InvoiceListRecord = props => {
var invoiceData = JSON.parse(props.data[props.childDataCollection]);
return invoiceData.map((invKey, index) => {
return (
<div className="col-3 mb-1" key={"item-data-" + index}>
<strong>{invKey.MappedFieldName}</strong>
<br />
{invKey.Value}
</div>
);
});
};
export default InvoiceListRecord;
The API is not called more than once if the url is the same. It just gets the value from data variable. The api call is not made again, unless the url changes.
I created an example from your code, changing all the unknown components to div. I added a console.log in the useEffect of the useAxiosFetch hook. And to re-render the component, I added a button to increment the count.
You'll see that the console.log from the hook is printed only once, even though the component re-renders on every button click. The value just comes from the data variable from the hook and the api call is not made again and again.

Resources