i am trying to put a button of edit for each item with the help of simple hook. i have used map to see the elements of an item.... but somehow it's not working...while pressing the button nothing is showing.....
Codesandbox link: https://codesandbox.io/s/condescending-worker-s0igh
app.js:
import React, { useState } from "react";
import TodoList from "./TodoFiles/TodoList";
const defaultItems = [
{ id: 1, title: "Write React Todo Project", completed: true },
{ id: 2, title: "Upload it to github", completed: false }
];
const App = () => {
const [items,setItems]=useState(defaultItems)
const editItem=({id,title})=>{
setItems(items.map(p=>p.id===id?{...p,title}:p))
}
return (
<div style={{ width: 400 }}>
<hr />
<TodoList
items={items}
editItem={editItem}/>
<hr />
</div>
);
};
export default App;
TodoList.js:
import React from "react";
const TodoItem = ({ title, completed, editItem }) => {
return (
<div style={{ width: 400, height: 25 }}>
<input type="checkbox" checked={completed} />
{title}
<button style={{ float: "right" }} onClick={() => editItem(title)}>
Edit
</button>
</div>
);
};
const TodoList = ({ items = [], index,editItem }) => {
return items.map((p, index) => (
<TodoItem
{...p}
key={p.id}
index={index}
editItem={editItem}
/>
));
};
export default TodoList;
i don't want to use useEffect or useReducer fro custom hook ... because i want to practice with basics. sorry for my frequent questions, i'm trying so hard to learn this reactjs and i dun want to give up... thanx in advance and if it is possible, will you put some explanations in a plain text, why it's not working.
You forgot to pass id to editItem function on button onClick in TodoItem component.
Also you shouldn't use old state in setState function like that (in editItem function). You should use an updater function argument. Thanks to that you always modify current state.
const editItem = ({id,title}) => {
setItems(oldItems => (olditems.map(p=>p.id===id?{...p,title}:p)))
}
import React from "react";
const TodoItem = ({id, title, completed, editItem }) => {
return (
<div style={{ width: 400, height: 25 }}>
<input type="checkbox" checked={completed} />
{title}
<button style={{ float: "right" }} onClick={() => editItem({id,title})}>
Edit
</button>
</div>
);
};
const TodoList = ({ items = [], index,editItem }) => {
return items.map((p, index) => (
<TodoItem
{...p}
key={p.id}
index={index}
editItem={editItem}
/>
));
};
export default TodoList;
Related
I'm trying to update value in react functional compoenent through input element but after the first value I'm unable to type
My Code:
import React from "react";
import "./App.css";
const { useState } = React;
function App() {
const [items, setItems] = useState([
{ value: "" },
{ value: "" },
{ value: "" },
]);
const [count, setCount] = useState(0);
const Item = React.memo(({ id, value, onChange }) => {
return (
<div className="item">Item
<input
onChange={(e) => onChange(id, e.target.value)}
value={value}
/>
</div>
);
});
return (
<div className="app">
<h1>Parent</h1>
<p style={{marginTop: '20px'}}>Holds state: {count}, Does't passes to it items</p>
<p style={{marginTop: '20px'}}>{JSON.stringify(items)}</p>
<button onClick={() => setCount((prev) => prev + 1)} style={{marginTop: '20px'}}>
Update Parent
</button>
<ul className="items" style={{marginTop: '20px'}}>
{items.map((item, index) => {
return (
<Item
key={index}
id={index}
value={item.value}
onChange={(id, value) =>
setItems(
items.map((item, index) => {
return index !== id
? item
: { value: value };
})
)
}
/>
);
})}
</ul>
</div>
);
}
export default App;
You should move the Item declaration outside the App component. Having a component declaration inside another one is almost always a bad idea. Explanation below.
import React, { useState } from "react";
const Item = React.memo(({ id, value, onChange }) => {
return (
<div className="item">
Item
<input onChange={(e) => onChange(id, e.target.value)} value={value} />
</div>
);
});
function App() {
const [items, setItems] = useState([
{ value: "" },
{ value: "" },
{ value: "" }
]);
const [count, setCount] = useState(0);
return (
<div className="app">
<h1>Parent</h1>
<p style={{ marginTop: "20px" }}>
Holds state: {count}, Does't passes to it items
</p>
<p style={{ marginTop: "20px" }}>{JSON.stringify(items)}</p>
<button
onClick={() => setCount((prev) => prev + 1)}
style={{ marginTop: "20px" }}
>
Update Parent
</button>
<ul className="items" style={{ marginTop: "20px" }}>
{items.map((item, index) => {
return (
<Item
key={index}
id={index}
value={item.value}
onChange={(id, value) =>
setItems(
items.map((item, index) => {
return index !== id ? item : { value: value };
})
)
}
/>
);
})}
</ul>
</div>
);
}
export default App;
When a component definition is inside another component, React will re-declare the inner component every time the parent re-renders. This means, that any state held by the inner component will be lost.
In your case, since every time there is an entirely new component, the input was not the same input as in the previous render. This means that the input that was in focus in the previous render is not present anymore, and the new input is not focused anymore.
You should also probably change
setItems(
items.map((item, index) => {
return index !== id ? item : { value: value };
})
)
to
prev.map((item, index) => {
return index !== id ? item : { value: value };
})
)
It's a good idea to use the function notation for set state when the new state depends on the old state value.
I have a datagrid table in which I'm getting my database data from an API call and I have written the table code in one file. I also have a search functionality where you can search for a particular record inside the table, but this search code is in another file. I am having difficulty of passing my state variable containing the search parameter from my search file to the table file. I have separated all my components in different pages since it'd be easier to structure them using a grid in my App.js. How do I get my search query to my table file next?
My search code:
export default function SearchInput() {
const [searchTerm, setSearchTerm] = React.useState('');
return (
<Grid item xs={3}>
<Box mt={1.6}
component="form"
sx={{
'& > :not(style)': { m: 1, width: '20ch', backgroundColor: "white", borderRadius: 1},
}}
noValidate
autoComplete="off"
>
<TextField
placeholder="Search Customer ID"
variant="outlined"
size="small"
sx={{input: {textAlign: "left"}}}
onChange={(event) => {
setSearchTerm(event.target.value);
console.log(searchTerm);
}}
/>
</Box>
</Grid>
);
}
My table code:
export default function DataTable() {
const [pageSize, setPageSize] = React.useState(10);
const [data, setData] = React.useState([]);
useEffect(async () => {
setData(await getData());
}, [])
return (
<div style={{ width: '100%' }}>
<DataGrid
rows={data}
columns={columns}
checkboxSelection={true}
autoHeight={true}
density='compact'
rowHeight='40'
headerHeight={80}
disableColumnMenu={true}
disableSelectionOnClick={true}
sx={datagridSx}
pageSize={pageSize}
onPageSizeChange={(newPageSize) => setPageSize(newPageSize)}
rowsPerPageOptions={[5, 10, 15]}
pagination
/>
</div>
);
}
App.js
function App() {
return (
<div className="App">
<Container maxWidth="false" disableGutters="true">
<Grid container spacing={0}>
<ABClogo />
<HHHlogo />
</Grid>
<Grid container spacing={0}>
<LeftButtonGroup />
<SearchInput />
<RightButtonGroup />
</Grid>
<Grid container spacing={0}>
<DataTable />
<TableFooter />
</Grid>
</Container>
</div>
);
}
Here is a minimal example using createContext(), and useReducer() to lift up state and share it between components, similar to what you are after, but as jsNoob says, there are multiple options. This is one I'm comfortable with.
The concept is explained here: https://reactjs.org/docs/context.html
Essentially you can create 'global' state at any point in your component tree and using Provider / Consumer components, share that state and functionality with child components.
//Main.js
import React, { createContext, useContext, useReducer } from 'react';
const MainContext = createContext();
export const useMainContext => {
return useContext(MainContext);
}
const mainReducer = (state, action) => {
switch(action.type){
case: 'SOMETHING':{
return({...state, something: action.data});
}
default:
return state;
}
}
export const Main = () => {
const [mainState, mainDispatch] = useReducer(mainReducer, {something: false});
const stateOfMain = { mainState, mainDispatch };
return(
<MainContext.Provider value={stateOfMain}>
<MainContext.Consumer>
{() => (
<div>
<Nothing />
<Whatever />
</div>
)}
</MainContext.Consumer>
</MainContext.Provider>
)
}
Then you can have the other components use either or both of the main state and dispatch.
//Nothing.js
import {mainContext} from './Main.js'
const Nothing = () => {
const { mainState, mainDispatch } = useMainContext();
return(
<button onClick={() => {mainDispatch({type: 'SOMETHING', data: !mainState.something})}}></button>
)
}
Clicking the button in the above file, should change the display of the below file
//Whatever.js
import {mainContext} from './Main.js'
const Whatever = () => {
const { mainState } = useMainContext();
return(
<div>{mainState.something}</div>
);
}
I'm using MUI library to create my React Js app.
Here I'm using the controlled Text Field component to create a simple search UI.
But there is something strange. The Text Field component loses focus after its value is changed.
This is my first time facing this problem. I never faced this problem before.
How this could happen? And what is the solution.
Here is the code and the playground: https://codesandbox.io/s/mui-autocomplete-lost-focus-oenoo?
Note: if I remove the breakpoint type from the code, the Text Field component still loses focus after its value is changed.
It's because you're defining a component inside another component, so that component definition is recreated every time the component renders (and your component renders every time the user types into the input).
Two solutions:
Don't make it a separate component.
Instead of:
const MyComponent = () => {
const MyInput = () => <div><input /></div>; // you get the idea
return (
<div>
<MyInput />
</div>
);
};
Do:
const MyComponent = () => {
return (
<div>
<div><input /></div> {/* you get the idea */}
</div>
);
};
Define the component outside its parent component:
const MyInput = ({value, onChange}) => (
<div>
<input value={value} onChange={onChange} />
</div>
);
const MyComponent = () => {
const [value, setValue] = useState('');
return (
<div>
<MyInput
value={value}
onChange={event => setValue(event.target.value)}
/>
</div>
);
};
For MUI V5
Moved your custom-styled code outside the component
For example:
import React from 'react';
import { useTheme, TextField, styled } from '#mui/material';
import { SearchOutlined } from '#mui/icons-material';
interface SearchInputProps { placeholder?: string; onChange: (event: React.ChangeEvent<HTMLInputElement>) => void; value: string; dataTest?: string; }
const StyledSearchInput = styled(TextField)(({ theme }: any) => { return {
'& .MuiOutlinedInput-root': {
borderRadius: '0.625rem',
fontSize: '1rem',
'& fieldset': {
borderColor: `${theme.palette.text.secondary}`
},
'&.Mui-focused fieldset': {
borderColor: `${theme.palette.primary}`
}
} }; });
const SearchInput: React.FC<SearchInputProps> = ({ placeholder = 'Search...', onChange, value, dataTest, ...props }) => { const theme = useTheme();
return (
<StyledSearchInput
{...props}
onChange={onChange}
placeholder={placeholder}
variant="outlined"
value={value}
inputProps={{ 'data-testid': dataTest ? dataTest : 'search-input' }}
InputProps={{
startAdornment: (
<SearchOutlined
sx={{ color: theme.palette.text.secondary, height: '1.5rem', width: '1.5rem' }}
/>
)
}}
/> ); };
export default SearchInput;
Be careful about your components' keys, if you set dynamic value as a key prop, it will also cause focus lost. Here is an example
{people.map(person => (
<div key={person.name}>
<Input type="text" value={person.name} onChange={// function which manipulates person name} />
</div>
))}
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 (...)
}
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>
)
}