Disconnect between UI and backend data - javascript

I'm having trouble with a PUT request updating properly. I have a table titled 'issues' with the following columns:
title: title of issue,
description: description of issue,
zipcode: zipcode of issue,
upvotes: number of upvotes an issue receives // defaults to 0
For each issue created, a card is displayed that presents the information above. I want to be able to click on a button that increases the upvote column by +1 on every click. Right now, there is a disconnect between the click and the actual data. The UI's number is always 1 ahead of the backend. If the UI displays 17, for example, the backend reads 16, and so on. I've tried a bunch of different combinations thus far to no avail.
I'm using React and Node/Express for this project.
A thought:
I didn't know if it was related to the my component hierarchy or not, but include both components just in case.
Below is the code my for the card displaying the individual 'issues':
/** #jsx jsx */
import React, { useState } from 'react';
import { jsx } from '#emotion/core';
import axios from 'axios';
import { Button, Card } from 'semantic-ui-react'
function IssueCard({ issue }) {
const [currentIssue, setCurrentIssue] = useState(issue);
const [editedIssue, setEditedIssue] = useState(currentIssue);
// Local storage management
const token = window.localStorage.getItem('token');
// Issue ID to be used as dynamic param
const id = issue.id;
console.log("EDITED ISSUE", editedIssue);
// Once chevron is clicked, the # of upvotes increases by 1
function upvoteIssue() {
// Change state to +1 for upvote
setEditedIssue({ ...editedIssue, upvotes: editedIssue.upvotes + 1 });
axios
.put(`http://localhost:3000/issues/${id}`, editedIssue, {
headers: {
Authorization: token
}
})
.then(response => {
// When I log the response, the first click doesn't increase upvote, but the second click does
console.log("RESPONSE", response.data)
setCurrentIssue(response.data)
})
.catch(error => {
console.log(error);
})
};
return (
<>
<Card>
<Card.Content>
<Card.Header>{ issue.title }</Card.Header>
<Card.Meta>{ issue.zipcode }</Card.Meta>
<Card.Description>
{ issue.description }
</Card.Description>
</Card.Content>
<div
css={{
marginLeft: '12px',
marginBottom: '10px'
}}
>
<Button
size='huge'
icon='heart'
label={{ as: 'p', basic: true, content: editedIssue.upvotes }}
labelPosition='right'
onClick={upvoteIssue}
/>
</div>
</Card>
</>
)
};
export default IssueCard;
Below is the code for the parent component, which is just a profile page that lists the issues if there are any:
/** #jsx jsx */
import React, { useState, useEffect } from 'react';
import { css, jsx } from '#emotion/core';
import { useTheme } from 'emotion-theming';
import axios from 'axios';
import { Link } from 'react-router-dom';
import Banner from './Banner';
import { Button, Card, Icon, Image } from 'semantic-ui-react'
import IssueCard from './IssueCard';
import profile_placeholder from '../images/profile_placeholder.png';
function Profile(props) {
const [currentUser, setCurrentUser] = useState("");
const [issues, setIssues] = useState([]);
const [isEditingUser, setIsEditingUser] = useState(false);
const [isEditingIssue, setIsEditingIssue] = useState(false);
const [issueToUpdate, setIssueToUpdate] = useState({})
let token = window.localStorage.getItem('token')
let id = window.localStorage.getItem('id')
// Importing theme colors
const theme = useTheme();
useEffect(() => {
fetchUser();
fetchIssues();
}, [])
// Fetches user data to populate profile card with proper information
function fetchUser() {
axios
.get(`http://localhost:3000/users/${id}`, {
headers: {
Authorization: token
}
})
.then(response => {
setCurrentUser(response.data);
})
.catch(error => {
console.log(error);
})
};
// Fetches the issues created by the logged user
function fetchIssues() {
axios
.get(`http://localhost:3000/users/${id}/issues`, {
headers: {
Authorization: token
}
})
.then(response => {
setIssues(response.data);
})
.catch(err => {
console.log(err)
})
};
return (
<>
<Banner />
<div
css={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginBottom: '50px'
}}
>
<div
css={{
display: 'flex',
marginTop: '50px'
}}
>
<Card>
<Image src={profile_placeholder} wrapped ui={false} />
<Card.Content>
<Card.Header>{ currentUser.username }</Card.Header>
<Card.Meta>{ currentUser.email }</Card.Meta>
<Card.Description>
{ currentUser.biography }
</Card.Description>
</Card.Content>
<Card.Content extra>
<a>
<Icon name='user' />
Posted Issues: {currentUser.posted_issues}
</a>
</Card.Content>
</Card>
</div>
<div
css={{
display: 'flex',
marginTop: '20px'
}}
>
<Link to="/addIssue">
<Button
icon
labelPosition="left"
color="facebook"
size="huge"
>
<Icon name="add" />
Add Issue
</Button>
</Link>
<Button
icon
labelPosition="left"
color="facebook"
size="huge"
>
<Icon name="redo" />
Edit Profile
</Button>
</div>
<div
css={{
marginTop: '20px',
backgroundColor: theme.colors.turquoise,
width: '50%'
}}
>
<h3
css={{
margin: '0px 10px',
color: theme.colors.white
}}
>Issues created by { currentUser.username }:</h3>
</div>
{issues.map(issue => <IssueCard issue={issue} key={issue.id} /> )}
</div>
</>
)
}
export default Profile;

Related

How to use map in react component using nextjs app?

I have data returned from the backend as an array that i want to populate on react component.
home.js
import Head from "next/head";
import Header from "../src/components/Header";
import * as React from 'react';
import { styled } from '#mui/material/styles';
import Box from '#mui/material/Box';
import Paper from '#mui/material/Paper';
import Grid from '#mui/material/Grid';
import TextField from '#mui/material/TextField';
import SendIcon from '#mui/icons-material/Send';
import Stack from '#mui/material/Stack';
import Button from '#mui/material/Button';
import getDupImages from "../src/services/getDupImages";
import { useState, useEffect } from 'react'
const Item = styled(Paper)(({ theme }) => ({
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
...theme.typography.body2,
padding: theme.spacing(1),
textAlign: 'center',
color: theme.palette.text.secondary,
}));
export default function Home({data}) {
let _data;
const fetchData = async () => {
_data = await getDupImages();
console.log("DATA>>>", _data);
return _data;
};
const submit = (event) => {
event.preventDefault();
fetchData();
}
return (
<>
<Head>
<title>De-Dup</title>
<link rel="icon" type="image/ico" href="/img/goals.ico" />
</Head>
<Header />
<section>
<Box sx={{ flexGrow: 1 }}>
<Grid container spacing={2}>
<Grid item xs={5}>
<Box
component="form"
sx={{
'& > :not(style)': { m: 1, width: '75ch' },
}}
noValidate
autoComplete="off"
>
<TextField id="outlined-basic" label="location path" variant="outlined" />
<Stack direction="row" spacing={2}>
<Button variant="contained" onClick={submit} endIcon={<SendIcon />}>
Submit
</Button>
</Stack>
</Box>
</Grid>
<Grid item xs={7}>
{data.map((d) => {
return (
<div>
{d.title}
</div>
);
})}
</Grid>
</Grid>
</Box>
</section>
</>
);
}
Error
1 of 1 unhandled error
Server Error
TypeError: Cannot read property 'map' of undefined
This error happened while generating the page. Any console logs will be displayed in the terminal window.
Put the data in the component state and check if there actually is data before displaying it.
const [data, setData] = useState();
const fetchData = async () => {
setData(await getDupImages());
}
Then in your JSX:
{!!data && data.map(d => <div>{d.title}</div>}
You are trying to render the data before it is available. Use this instead
{data && data.map((d) => {
return (
<div>
{d.title}
</div>
);
})}
Either initialise the data state as an array or use the Optional chaining (?.) operator before the map function:
data?.map((d) => {
return <div>{d.title}</div>;
})
Hope this helps.

Why is there an error when refreshing the page? react

after i login in user page i try to refresh page -it throw an error ?
here is my dashboard, in my application is already running, it works fine, but already on the user page when i try to refresh it i got the error
here is my code
https://codesandbox.io/s/serene-https-oi6hw
the error is :
Error: Objects are not valid as a React child (found: object with keys {}). If you meant to render a collection of children, use an
array instead.
import { useSelector } from "react-redux";
//react-redux actions
import { Userinfo } from "../auth/actions/userActions";
import { logoutUser } from "../auth/actions/userActions";
//history
import { useHistory } from "react-router-dom";
const Dashboard = () => {
const history = useHistory();
const selectUser = (state) => state.user;
const user = useSelector(selectUser);
return (
<div>
<div
style={{
position: "absolute",
top: 0,
left: 0,
backgroundColor: "transparent",
width: "100%",
padding: "15px",
display: "flex",
justifyContent: "flex-start"
}}
>
<Avatar image={Logo} />
</div>
<StyledFromArea bg={colors.dark2}>
<StyledTitle size={65}>Hello, {user}</StyledTitle>
<Userinfo />
<ButtonGroup>
<StyledButton to="#" onClick={() => logoutUser(history)}>
Logout
</StyledButton>
</ButtonGroup>
</StyledFromArea>
</div>
);
};
export default Dashboard;
Here is the problem
<StyledTitle size={65}>Hello, {user}</StyledTitle>
user is an object.
Try Hello, {user?.email} or the property you wanted to display
Try this:
import { useSelector } from "react-redux";
//react-redux actions
import { Userinfo } from "../auth/actions/userActions";
import { logoutUser } from "../auth/actions/userActions";
//history
import { useHistory } from "react-router-dom";
const Dashboard = () => {
const history = useHistory();
const selectUser = (state) => state.user;
const user = useSelector(selectUser);
return (
<div>
<Avatar image={Logo} />
<StyledFromArea bg={colors.dark2}>
<StyledTitle size={65}>Hello, {user}</StyledTitle>
<Userinfo />
<ButtonGroup>
<StyledButton to="#" onClick={() => logoutUser(history)}>
Logout
</StyledButton>
</ButtonGroup>
</StyledFromArea>
</div>
);
};
export default Dashboard;

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

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

using useparams and array.find to display detailed data

I am making a small blog application using react js. I have a context api for the user inputs, so that the data can be used globally across components (InputContext.js). Using react router, the user is able to view a list of all blog entries (AllBlogs.js) and view each one of them in detail (BlogDetail.js). What I am trying to achieve is, allow the user to get a detailed view of an individual blog post component from the AllBlogs.js page. All blogs have an "id" property, which is used to query the url and using the array.find method, it is supposed to show a detailed view of the blog with the matching id. The problem is "findBlogs" in BlogDetails that is being passed as a prop to display the detailed individual blog data always only returns the most recent user input value, therefore all blogs show the exact same information. I am unsure as to why this is happening, any guidance towards the right direction is greatly appreciated.
InputContext.js
import React, { useState, createContext, useMemo } from 'react'
//create context
export const InputContext = createContext();
const InputContextProvider = (props) => {
const [blogPost, setBlogPost] = useState({
id: '',
title: '',
author: '',
text: ''
});
//create an array to push all the blogPosts
const [allBlogPosts, setAllBlogPosts] = useState([]);
console.log(allBlogPosts)
//put value inside useMemo so that the component only rerenders when there is change in the value
const value = useMemo(() => ({ blogPost, setBlogPost, allBlogPosts, setAllBlogPosts }), [blogPost, allBlogPosts])
return (
<InputContext.Provider value={value}>
{props.children}
</InputContext.Provider>
)
}
export default InputContextProvider;
WriteBlogPost.js
import React, { useState, useContext, Fragment } from 'react'
import { useHistory } from 'react-router-dom'
import { InputContext } from '../Contexts/InputContext'
import { TextareaAutosize } from '#material-ui/core'
import { v4 as uuidv4 } from 'uuid';
import { Box, TextField, Button, makeStyles } from '#material-ui/core'
const useStyles = makeStyles({
root: {
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center'
}
})
export const WriteBlogPost = () => {
const classes = useStyles();
const [blog, setBlog] = useState({
id: '',
title: '',
author: '',
text: ''
});
const history = useHistory();
const { setBlogPost } = useContext(InputContext);
const { allBlogPosts, setAllBlogPosts } = useContext(InputContext)
const handleBlogPost = () => {
setBlogPost(blog);
setAllBlogPosts([...allBlogPosts, blog]);
history.push("/blogs")
console.log({ blog })
console.log({ allBlogPosts })
}
const handleChange = (e) => {
const value = e.target.value
setBlog({
...blog,
id: uuidv4(),
[e.target.name]: value
})
}
return (
<Fragment>
<Box className={classes.root}>
<div>
<TextField id="standard-basic" onChange={handleChange} value={blog.title} name="title" label="Title" />
</div>
<div>
<TextField id="standard-basic" onChange={handleChange} value={blog.author} name="author" label="Author" />
</div>
<div>
<TextareaAutosize aria-label="minimum height" minRows={20} style={{ width: '70%' }} placeholder="Your blog post"
onChange={handleChange}
value={blog.text}
name="text" />
</div>
<div>
<Button variant="contained" color="primary" onClick={handleBlogPost}>
Submit</Button>
</div>
</Box>
</Fragment>
)
}
AllBlogs.js
import React, { useContext } from 'react'
import { InputContext } from '../Contexts/InputContext'
import { Card, CardContent, Typography } from '#material-ui/core'
import { makeStyles } from '#material-ui/core'
import { Link } from 'react-router-dom'
const useStyles = makeStyles({
root: {
justifyContent: 'center',
alignItems: 'center',
display: 'flex',
textAlign: 'center',
},
text: {
textAlign: 'center'
}
})
export const AllBlogs = () => {
const classes = useStyles();
const { allBlogPosts, blogPost } = useContext(InputContext)
console.log(allBlogPosts)
return (
<div>
<Typography color="textPrimary" variant="h3" className={classes.text}>All blogs</Typography>
{allBlogPosts.map((post, i) =>
<Card variant="outlined" key={i} className={classes.root}>
<CardContent>
<Typography color="textPrimary" variant="h5">
{post.title}
</Typography>
<Typography color="textPrimary" variant="h6">
{post.author}
</Typography>
<Typography color="textPrimary" variant="body2" component="p">
{post.text}
</Typography>
<Link to={`/blogs/${blogPost.id}`}>
Read blog
</Link>
</CardContent>
</Card>
)}
</div>
)
}
BlogDetail.js
import React, { useContext } from 'react'
import { useParams, Route } from 'react-router'
import { SingleBlog } from './SingleBlog';
import { InputContext } from '../Contexts/InputContext';
export const BlogDetail = () => {
const params = useParams();
console.log(params.blogId)
const { allBlogPosts } = useContext(InputContext)
const findBlog = allBlogPosts.find((post) => post.id === params.blogId)
console.log(findBlog)
if (!findBlog) {
return <p>No blogs found.</p>
}
return (
<div>
<h1>Blog details</h1>
<SingleBlog post={findBlog} />
</div>
)
}
Issue
Ah, I see what is happening... had to dig back through your edits to when you included your context code.
In your provider you for some reason store an array of blogs (this part makes sense), but then you also store the last blog that was edited.
const InputContextProvider = (props) => {
const [blogPost, setBlogPost] = useState({
id: '',
title: '',
author: '',
text: ''
});
//create an array to push all the blogPosts
const [allBlogPosts, setAllBlogPosts] = useState([]);
//put value inside useMemo so that the component only rerenders when there is change in the value
const value = useMemo(() => ({
blogPost, // <-- last blog edited
setBlogPost,
allBlogPosts,
setAllBlogPosts
}), [blogPost, allBlogPosts])
return (
<InputContext.Provider value={value}>
{props.children}
</InputContext.Provider>
)
}
export const WriteBlogPost = () => {
...
const [blog, setBlog] = useState({
id: '',
title: '',
author: '',
text: ''
});
...
const { setBlogPost } = useContext(InputContext);
const { allBlogPosts, setAllBlogPosts } = useContext(InputContext)
const handleBlogPost = () => {
setBlogPost(blog); // <-- saves last blog edited/added
setAllBlogPosts([...allBlogPosts, blog]);
history.push("/blogs");
}
const handleChange = (e) => {
const value = e.target.value
setBlog({
...blog,
id: uuidv4(),
[e.target.name]: value
})
}
return (
...
)
}
When you are mapping the blog posts you form incorrect links.
export const AllBlogs = () => {
const classes = useStyles();
const {
allBlogPosts,
blogPost // <-- last blog updated
} = useContext(InputContext);
return (
<div>
...
{allBlogPosts.map((post, i) =>
<Card variant="outlined" key={i} className={classes.root}>
<CardContent>
...
<Link to={`/blogs/${blogPost.id}`}> // <-- link last blog updated id
Read blog
</Link>
</CardContent>
</Card>
)}
</div>
)
}
Solution
Use the current blog post's id when mapping to form the link correctly.
export const AllBlogs = () => {
const classes = useStyles();
const { allBlogPosts } = useContext(InputContext);
return (
<div>
...
{allBlogPosts.map((post, i) =>
<Card variant="outlined" key={post.id} className={classes.root}>
<CardContent>
...
<Link to={`/blogs/${post.id}`}> // <-- link current post id
Read blog
</Link>
</CardContent>
</Card>
)}
</div>
)
}

Bold active menu after refreshing the page

I have an application has been written with React + Redux and Antdesign. My application is a dashboard app. So I used the layout in Ant design https://ant.design/components/layout/
When I click on side menus, the active menu gets bold which is fine. But I need when I refresh the page, it checks and detect the route and bold related menu item.
I have a Sidebar component which is stateful. Inside it, in componentDidMount I call a function which will dispatch an action from mapDispatchToProps. The reducer changes the state. But in HTML codes, in defaultSelectedKeys, I can not set the number of active menus.
Sidebar.js component:
import React from 'react';
import { render } from 'react-dom';
import { connect } from 'react-redux'
import { Switch, BrowserRouter, Route, Link } from 'react-router-dom';
// antd
import { Layout, Breadcrumb, Menu, Icon } from 'antd';
const { Header, Content, Footer, Sider } = Layout;
// Helpers
import { Alert } from '../helpers/notifications';
// Components
import Home from '../components/Home';
// import Header from '../components/Header';
import NotFound from '../components/NotFound';
import PostsEditor from '../components/Posts/PostsEditor';
// Actions
import { setRouteActiveFlag } from '../actions/ui.action'
class Sidebar extends React.Component {
componentDidMount () {
const routes = {
'/' : 1,
'/posts' : 2,
'/logout' : 3
}
this.props.detectActiveRoute(setRouteActiveFlag({
routes:routes,
path:window.location.pathname
}))
}
render() {
const { selectedRoute } = this.props;
console.log(selectedRoute);
return (
<div>
<Layout>
<Sider
style={{
overflow: 'auto',
height: '100vh',
position: 'fixed',
left: 0,
}}
breakpoint="lg"
collapsedWidth="0"
onBreakpoint={broken => {
console.log(broken);
}}
onCollapse={(collapsed, type) => {
console.log(collapsed, type);
}}
>
<div className="logo" >
Logo <br/><br/><br/>
</div>
<Menu theme="dark" mode="inline" style={{ lineHeight: '64px' }} defaultSelectedKeys={[selectedRoute.toString() || '1']}>
<Menu.Item key="1">
<Link to="/" style={{ color:'#fff' }}>
<Icon type="user" />
<span className="nav-text">Home</span>
</Link>
</Menu.Item>
<Menu.Item key="2">
<Link to="/posts" style={{ color:'#fff' }}>
<Icon type="user" />
<span className="nav-text">Posts</span>
</Link>
</Menu.Item>
<Menu.Item key="3">
<a href="/logout" style={{ color:'#fff' }}>
<Icon type="user" />
<span className="nav-text">Logout</span>
</a>
</Menu.Item>
</Menu>
</Sider>
<Layout style={{ marginLeft: 200 }}>
<Content style={{ margin: '24px 16px 0', overflow: 'initial'}}>
<Breadcrumb style={{ margin: '0 0 20px 0' }}>
<Breadcrumb.Item>Home</Breadcrumb.Item>
<Breadcrumb.Item>List</Breadcrumb.Item>
<Breadcrumb.Item>App</Breadcrumb.Item>
</Breadcrumb>
<div style={{ padding: 24, background: '#fff', minHeight: 360 }}>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/posts/:id?" component={PostsEditor} />
<Route component={NotFound}/>
</Switch>
<Alert stack={ { limit: 3 } } />
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>Ant Design ©2018 Created by Ant UED</Footer>
</Layout>
</Layout>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
return {
state: state,
props: ownProps,
selectedRoute:state.ui.selectedRoute || 1
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
detectActiveRoute: (obj) => dispatch(obj)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Sidebar)
ui.action.js
export const setRouteActiveFlag = (payload = 'global') => ({
type: actions.SET_ROUTE_ACTIVE_FLAG,
payload
});
ui.reducer.js
import { handleActions } from 'redux-actions';
import Immutable from 'seamless-immutable';
import * as actions from '../consts/action-types';
const initialState = Immutable({
requests: {},
selectedRoute:{}
});
export default handleActions({
[actions.SET_ROUTE_ACTIVE_FLAG]: (state, action) => {
if (action.payload.routes && action.payload.path && action.payload.routes[ action.payload.path ]) {
return state.set('selectedRoute', action.payload.routes[ action.payload.path ])
}else{
return state.set('selectedRoute', 1)
}
}
}, initialState);
Please help me find the best and simple practices.
There is no need to use redux, just use react-router to get current pathname and pass it to defaultSelectedKeys.
<Menu defaultSelectedKeys={[this.props.location.pathname]}>
...
.....
</Menu>
Look at this answer , if you don't know how to get pathname
The following answer assumes you are using hooks. I know that from your question you are not using hooks, but it could be useful for other people. This answer works not only when refreshing but also when pressing the back and forward buttons:
import React, { useState, useEffect } from 'react'
import { useHistory, useLocation } from 'react-router-dom'
import { Layout, Menu } from 'antd'
const { Sider } = Layout
const items = [
{ key: '1', label: 'Invoices', path: '/admin/invoices' },
{ key: '2', label: 'Service Details', path: '/admin/service-details' },
{ key: '3', label: 'Service Contract Details', path: '/admin/service-contract-details' },
{ key: '4', label: 'Cost Centers', path: '/admin/cost-centers' },
{ key: '5', label: 'Clients', path: '/admin/clients' },
{ key: '6', label: 'Vendors', path: '/admin/vendors' }
]
const Sidebar = () => {
const location = useLocation()
const history = useHistory()
const [selectedKey, setSelectedKey] = useState(items.find(_item => location.pathname.startsWith(_item.path)).key)
const onClickMenu = (item) => {
const clicked = items.find(_item => _item.key === item.key)
history.push(clicked.path)
}
useEffect(() => {
setSelectedKey(items.find(_item => location.pathname.startsWith(_item.path)).key)
}, [location])
return (
<Sider style={{ backgroundColor: 'white' }}>
<h3 style={{ paddingLeft: '1rem', paddingTop: '1rem', fontSize: '1.25rem', fontWeight: 'bold', minHeight: 64, margin: 0 }}>
Costek
</h3>
<Menu selectedKeys={[selectedKey]} mode='inline' onClick={onClickMenu}>
{items.map((item) => (
<Menu.Item key={item.key}>{item.label}</Menu.Item>
))}
</Menu>
</Sider>
)
}
export default Sidebar
Your sidebar will look as follows:
You can add any css in your menu by conditioning and adding a class just in this way.
<MenuItem className={ (this.props.location.pathname==='/yourRoute')? 'active' : '' } >
</MenuItem>
In case if you get any kind of undefined error then you can use the 'withRouter' HOC
in this way.
In your component where you want to get that location prop, you will first import
import {withRouter} from 'react-router-dom';
then you can export it in this way.
export default withRouter(YourComponent);
Final code can look somewhat similar to this
import React, {Fragment, Component} from 'react';
import {withRouter, Link } from 'react-router-dom';
class Menu extends Component {
render(){
const {pathname} = this.props.location;
return (
<Fragment>
<div id="sidebar-menu" className="sidebar-menu">
<ul>
<li className={(pathname==='/dashboard' || pathname==='/')?'active':''}>
<Link to="/dashboard">Dashboard</Link>
</li>
<li className={(pathname==='/properties')?'active':''}>
<Link to="/properties">Properties</Link>
</li>
</ul>
</div>
</Fragment>
);
}
}
export default withRouter(Menu);

Categories