I developed a Simple React Application that read an external API and now I'm trying to develop a Like Button from each item. I read a lot about localStorage and persistence, but I don't know where I'm doing wrong. Could someone help me?
1-First, the component where I put item as props. This item bring me the name of each character
<LikeButtonTest items={item.name} />
2-Then, inside component:
import React, { useState, useEffect } from 'react';
import './style.css';
const LikeButtonTest = ({items}) => {
const [isLike, setIsLike] = useState(
JSON.parse(localStorage.getItem('data', items))
);
useEffect(() => {
localStorage.setItem('data', JSON.stringify(items));
}, [isLike]);
const toggleLike = () => {
setIsLike(!isLike);
}
return(
<div>
<button
onClick={toggleLike}
className={"bt-like like-button " + (isLike ? "liked" : "")
}>
</button>
</div>
);
};
export default LikeButtonTest;
My thoughts are:
First, I receive 'items' as props
Then, I create a localStorage called 'data' and set in a variable 'isLike'
So, I make a button where I add a class that checks if is liked or not and I created a toggle that changes the state
The problem is: I need to store the names in an array after click. For now, my app is generating this:
App item view
localStorage with name of character
You're approach is almost there. The ideal case here is to define your like function in the parent component of the like button and pass the function to the button. See the example below.
const ITEMS = ['item1', 'item2']
const WrapperComponent = () => {
const likes = JSON.parse(localStorage.getItem('likes'))
const handleLike = item => {
// you have the item name here, do whatever you want with it.
const existingLikes = likes
localStorage.setItem('likes', JSON.stringify(existingLikes.push(item)))
}
return (<>
{ITEMS.map(item => <ItemComponent item={item} onLike={handleLike} liked={likes.includes(item)} />)}
</>)
}
const ItemComponent = ({ item, onLike, liked }) => {
return (
<button
onClick={() => onLike(item)}
className={liked ? 'liked' : 'not-liked'}
}>
{item}
</button>
)
}
Hope that helps!
note: not tested, but pretty standard stuff
Related
So I have a filter chip, and this filter chip is just passed a text body, and close function like so:
import CloseIcon from '#mui/icons-material/Close';
import "./FilterChip.css";
function FilterChip({textBody, onCloseClick}) {
return <div className="filter-chip">
Category: {textBody} <CloseIcon onClick={onCloseClick} className="filter-chip-close-button"/>
</div>
}
export default FilterChip;
I can render multiple filter chips in one page. How can I tell my parent component that the particular chip's x button has been clicked? Is it possible to pass this data on the onCloseClick function? I need to remove the chip once it's x button has been clicked, and I also need to uncheck it from my list of check boxes in my parent component. This is how I render the chips.
function renderFilterChips() {
const checkedBoxes = getCheckedBoxes();
return checkedBoxes.map((checkedBox) =>
<FilterChip key={checkedBox} textBody={checkedBox} onCloseClick={onChipCloseClick} />
);
}
You should pass an "identifier" for each chip and then use that identifier to find out "what" was clicked by the user. And then you can filter out the clicked chip.
function FilterChip({ textBody, onCloseClick, id }) {
const handleOnClose = (event) => {
onCloseClick(event, id);
};
return (
<div className="filter-chip">
Category: {textBody}{" "}
<CloseIcon onClick={handleOnClose} className="filter-chip-close-button" />
</div>
);
}
Now your onCloseClick should accept a new param id and handle the logic to remove the chip .
Hope it helps.
Sounds like you need checkedBoxes to be in state.
import { useState } from "react"
const initialBoxes = getCheckedBoxes()
function renderFilteredChips() {
const [ checkedBoxes, setCheckedBoxes ] = useState(initialBoxes)
}
Then implement a function to remove a checked box by its index (or if you have a unique key identifier that would be even better)
const onChipCloseClick = (indexToRemove) => {
setCheckedBoxes(state => state.filter((_, chipIndex) => chipIndex !== indexToRemove))
}
Then when you map over the chips, make sure the function that closes the chip has its index, effectively allowing each chip in state to filter itself out of state, which will re-render your chips for you.
import { useState } from "react"
const initialBoxes = getCheckedBoxes()
function renderFilteredChips() {
const [ checkedBoxes, setCheckedBoxes ] = useState(initialBoxes)
const onChipCloseClick = (indexToRemove) => {
setCheckedBoxes(state => state.filter((_, chipIndex) => chipIndex !== indexToRemove))
}
return <>
{checkedBoxes.map((checkedBox, index) => (
<FilterChip
key={index}
textBody={checkedBox}
onCloseClick={() => onChipCloseClose(index)}
/>
})
</>
}
Obligatory note that I haven't checked this and wrote it in Markdown, so look out for syntax errors (:
This Codesandbox only has mobile styles as of now
I currently have a list of items being rendered based on their status.
Goal: When the user clicks on a nav button inside the modal, it updates the status type in context. Another component called SuggestionList consumes the context via useContext and renders out the items that are set to the new status.
Problem: The value in context is definitely being updated, but the SuggestionList component consuming the context is not re-rendering with a new list of items based on the status from context.
This seems to be a common problem:
Does new React Context API trigger re-renders?
React Context api - Consumer Does Not re-render after context changed
Component not re rendering when value from useContext is updated
I've tried a lot of suggestions from different posts, but I just cannot figure out why my SuggestionList component is not re-rendering upon value change in context. I'm hoping someone can give me some insight.
Context.js
// CONTEXT.JS
import { useState, createContext } from 'react';
export const RenderTypeContext = createContext();
export const RenderTypeProvider = ({ children }) => {
const [type, setType] = useState('suggestion');
const renderControls = {
type,
setType,
};
console.log(type); // logs out the new value, but does not cause a re-render in the SuggestionList component
return (
<RenderTypeContext.Provider value={renderControls}>
{children}
</RenderTypeContext.Provider>
);
};
SuggestionPage.jsx
// SuggestionPage.jsx
export const SuggestionsPage = () => {
return (
<>
<Header />
<FeedbackBar />
<RenderTypeProvider>
<SuggestionList />
</RenderTypeProvider>
</>
);
};
SuggestionList.jsx
// SuggestionList.jsx
import { RenderTypeContext } from '../../../../components/MobileModal/context';
export const SuggestionList = () => {
const retrievedRequests = useContext(RequestsContext);
const renderType = useContext(RenderTypeContext);
const { type } = renderType;
const renderedRequests = retrievedRequests.filter((req) => req.status === type);
return (
<main className={styles.container}>
{!renderedRequests.length && <EmptySuggestion />}
{renderedRequests.length &&
renderedRequests.map((request) => (
<Suggestion request={request} key={request.title} />
))}
</main>
);
};
Button.jsx
// Button.jsx
import { RenderTypeContext } from './context';
export const Button = ({ handleClick, activeButton, index, title }) => {
const tabRef = useRef();
const renderType = useContext(RenderTypeContext);
const { setType } = renderType;
useEffect(() => {
if (index === 0) {
tabRef.current.focus();
}
}, [index]);
return (
<button
className={`${styles.buttons} ${
activeButton === index && styles.activeButton
}`}
onClick={() => {
setType('planned');
handleClick(index);
}}
ref={index === 0 ? tabRef : null}
tabIndex="0"
>
{title}
</button>
);
};
Thanks
After a good night's rest, I finally solved it. It's amazing what you can miss when you're tired.
I didn't realize that I was placing the same provider as a child of itself. Once I removed the child provider, which was nested within itself, and raised the "parent" provider up the tree a little bit, everything started working.
So the issue wasn't that the component consuming the context wasn't updating, it was that my placement of providers was conflicting with each other. I lost track of my component tree. Dumb mistake.
The moral of the story, being tired can make you not see solutions. Get rest.
I have a react function component which sets an array of ids based on an user event on a link click(which opens a popup with some options that can be selected and has a callback once it is closed which will return the id of the selected element). these ids are passed to a child component which has a custom hook which uses these ids to perform some action. whenever i click on the link and select an element and close the popup.. get the error
"VM10715 react_devtools_backend.js:2430 You have changed a parameter while calling a hook which is supposed to remain unchanged [Array(2)]
0: (2) ["", "asdsadsad"]
lastIndex: (...)
lastItem: (...)
length: 1"
is there a way to make this work without running into this error? please see the code sample below
const TestComp = () => {
const [newIds, setNewIds] = useState([]);
const onPopupElementSelect = (ids) => {
setNewIds([...newIds, ids]);
};
return (
//renders some components
<>
<ImageComponent images={images} ids={newIds} onClick={handleClick} />
<Popup onSelect={onPopupElementSelect} />
</>
);
};
const ImageComponent = (props) => {
const { newIds, images } = props;
const newImages = useImages(ids || ['']); //customhook that fetches image details by ids
const imgs = images.map((i) => (
<div key={i.imageId}>
<img src={i.imageUrl} alt="" />
<Link onClick={handleClick} /> //opens the popup for user to select a new
image
</div>
));
return <div>{imgs}</div>;
};
ps: the paramerter names are not the issue.. this code is just a sample to give the basic idea of what i'm trying to do.
I think it is because you gave the same name to parameter and the state may be try newID as the parameter name
const onPopupElementSelect = (newId) => {
setIds(oldIds => [...oldIds, newId]);
};
I am a beginner in react native. I am stuck as I am unable to create a button which calls the function delBlogPost which would delete the last element in array of blogPosts. I want to use useState hook to delete the element. It would be helpful if anyone can help me with this. Here is the code:
BlogContext.js
import React, {useState} from 'react';
const BlogContext = React.createContext();
export const BlogProvider = ({ children }) => {
const [blogPosts, setBlogPosts] = useState([]);
const addBlogPost = () => {
setBlogPosts([...blogPosts, {title: `Blog Post #${blogPosts.length+1}`}]);
};
{/* I want to create a delBlogPost function here which would delete the last element in array of objects*/}
const delBlogPost = () => {
};
return (
<BlogContext.Provider value={{data: blogPosts, addBlogPost, delBlogPost }}>
{children}
</BlogContext.Provider>
);
};
export default BlogContext;
This is where I am creating the button to delete. Just like add blogpost button I want to have delete post button using useState.
IndexScreen.js
import React, { useContext } from 'react';
import { View, StyleSheet, Text, FlatList, Button} from 'react-native';
import BlogContext from '../context/BlogContext';
const IndexScreen = () => {
const { data, addBlogPost, delBlogPost} = useContext(BlogContext);
return (
<View>
<Text>Index Screen</Text>
<Button title = "Add Post" onPress ={addBlogPost}/>
<Button title = "Remove Post" onPress ={delBlogPost}/>
<FlatList
data={data}
keyExtractor={(blogPost) => blogPost.title}
renderItem={({ item }) => {
return <Text>{item.title}</Text>;
}}
/>
</View>
);
};
const styles = StyleSheet.create({});
export default IndexScreen;
The useState hook returns an array of values. The first element is always the value while the second value is always the setter function. State values using useState are immutable and cannot be directly modified, which is why you are given a setter. With this in mind, your addBlogPost and delBlogPost code should look like the following.
const [blogPosts, setBlogPosts] = React.useState([])
// since state is immutable, we can only set the value and not directly modify it
// we use the ... operator to add the current posts and then tack the new one at
// the end.
const addBlogPost = (blogPostToAdd) => setBlogPost([ ...blogPosts, blogPostToAdd])
const delBlogPost = (blogPostToRemove) => {
const blogPostsWithRemoved = blogPosts.filter((blogPost) => {
// you do not have to use id to identify which blog post you are removing,
// but you have to use something to identify the blog post you want to remove
return blogPost.id !== blogPostToRemove.id
})
}
Now when you use these functions, you will want to actually pass the blog post object to them.
<Button onClick={(event) => {
addBlogPost(blogPost)
}}>
Click Me to Add
</Button>
I have a parent component with an if statement to show 2 different types of buttons.
What I do, on page load, I check if the API returns an array called lectures as empty or with any values:
lectures.length > 0 ? show button A : show button B
This is the component, called main.js, where the if statement is:
lectures.length > 0
? <div onClick={() => handleCollapseClick()}>
<SectionCollapse open={open} />
</div>
: <LectureAdd dataSection={dataSection} />
The component LectureAdd displays a + sign, which will open a modal to create a new Lecture's title, while, SectionCollapse will show an arrow to show/hide a list of items.
The logic is simple:
1. On page load, if the lectures.lenght > 0 is false, we show the + sign to add a new lecture
OR
2. If the lectures.lenght > 0 is true, we change and show the collpase arrow.
Now, my issue happens when I add the new lecture from the child component LectureAdd.js
import React from 'react';
import { Form, Field } from 'react-final-form';
// Constants
import { URLS } from '../../../../constants';
// Helpers & Utils
import api from '../../../../helpers/API';
// Material UI Icons
import AddBoxIcon from '#material-ui/icons/AddBox';
export default ({ s }) => {
const [open, setOpen] = React.useState(false);
const [ lucturesData, setLecturesData ] = React.useState(0);
const { t } = useTranslation();
const handleAddLecture = ({ lecture_title }) => {
const data = {
"lecture": {
"title": lecture_title
}
}
return api
.post(URLS.NEW_COURSE_LECTURE(s.id), data)
.then(data => {
if(data.status === 201) {
setLecturesData(lucturesData + 1) <=== this doesn't trigger the parent and the button remains a `+` symbol, instead of changing because now `lectures.length` is 1
}
})
.catch(response => {
console.log(response)
});
}
return (
<>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
<AddBoxIcon />
</Button>
<Form
onSubmit={event => handleAddLecture(event)}
>
{
({
handleSubmit
}) => (
<form onSubmit={handleSubmit}>
<Field
name='lecture_title'
>
{({ input, meta }) => (
<div className={meta.active ? 'active' : ''}>
<input {...input}
type='text'
className="signup-field-input"
/>
</div>
)}
</Field>
<Button
variant="contained"
color="primary"
type="submit"
>
ADD LECTURE
</Button>
</form>
)}
</Form>
</>
)
}
I've been trying to use UseEffect to trigger a re-render on the update of the variable called lucturesData, but it doesn't re-render the parent component.
Any idea?
Thanks Joe
Common problem in React. Sending data top-down is easy, we just pass props. Passing information back up from children components, not as easy. Couple of solutions.
Use a callback (Observer pattern)
Parent passes a prop to the child that is a function. Child invokes the function when something meaningful happens. Parent can then do something when the function gets called like force a re-render.
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((lecture) => {
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Call API
let lecture = callApi();
// Notify parent of event
onLectureCreated(lecture);
}, [onLectureCreated]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Similar to solution #1, except for Parent handles API call. The benefit of this, is the Child component becomes more reusable since its "dumbed down".
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((data) => {
// Call API
let lecture = callApi(data);
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Create lecture data to send to callback
let lecture = {
formData1: '',
formData2: ''
}
// Notify parent of event
onCreateLecture(lecture);
}, [onCreateLecture]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Use a central state management tool like Redux. This solution allows any component to "listen in" on changes to data, like new Lectures. I won't provide an example here because it's quite in depth.
Essentially all of these solutions involve the same solution executed slightly differently. The first, uses a smart child that notifies its parent of events once their complete. The second, uses dumb children to gather data and notify the parent to take action on said data. The third, uses a centralized state management system.