I'm building an application focused on showing a user's github repositories and information. In a "Section" component I fetch these repositories and display them on the screen.
In the other component "Menu" I wanted it to count these repositories and display them. Should I use props in this case?
Section Component
import React, { useState } from 'react'
import axios from 'axios'
import { Square, Wrapper, Input, Button } from './Section.styled'
export default function Section() {
const [username, setUsername] = useState("");
const [loading, setLoading] = useState(false);
const [repos, setRepos] = useState([]);
const searchRepos = () => {
setLoading(true);
axios({
method: "get",
url: `https://api.github.com/users/${username}/repos`,
}).then(res => {
setLoading(false);
setRepos(res.data);
})
}
const handleSubmit = (e) => {
e.preventDefault();
searchRepos()
}
const renderRepo = (repo)=>{
return(
<Square>
{repo.name}
</Square>
)
}
return (
<>
<Wrapper>
<Input
placeholder="Usuário"
value={username}
onChange={e => { setUsername(e.target.value) }}
/>
<Button
onClick={handleSubmit}
type="submit">
{loading ? "Buscando..." : "Buscar"}
</Button>
{repos.map(renderRepo)}
</Wrapper>
</>
)
}
Menu Component
import React from "react";
import { bool } from "prop-types";
import { StyledMenu } from "./Menu.styled";
const Menu = ({ open, ...props }) => {
const isHidden = open ? true : false;
const tabIndex = isHidden ? 0 : -1;
return (
<>
<StyledMenu open={open} aria-hidden={!isHidden} {...props}>
<a href="/" tabIndex={tabIndex}>
Repositories:
</a>
<a href="/" tabIndex={tabIndex}>
Followeres:
</a>
<a href="/" tabIndex={tabIndex}>
Following:
</a>
</StyledMenu>
</>
);
};
Menu.propTypes = {
open: bool.isRequired,
};
export default Menu;
These solutions could be possible in this case when we have received data in one component and want it to appear in another component.
Pass the function searchRepos as a prop from the parent component of both section and menu component to section component, call the function from section, this will set data in parent, and send data to the menu component as props, i.e. called Lifting up the state.
If the components are far away (deeply nested or have unrelated parent, branch) you can simply make use of context store
3. Last way is to store the data of called API of the component section in browser local storage and use it in menu component. (NOT RECOMMENDED)
Related
I have a component that displays recipe data from an API, when the user clicks the button I am sending the recipe item ID up to Context with a recipeCtx.addRecipeID(id).
this button click also navigates the user to a page to display the recipe info.
const RecipeGridItem = ({title, image, id}) => {
const recipeCtx = useContext(RecipeContext);
const history = useHistory();
const recipeInfoSend = () => {
recipeCtx.addRecipeID(id);
history.push(`/info/${title.replace(/ /g,"-")}`);
}
return (
<div className={classes['grid-item']}>
<div className={classes['grid-item-inner']}>
<div className={classes['grid-front']}>
<img src={image} alt="" />
<h3>{title}</h3>
</div>
<div className={classes['grid-back']}>
{/* <p>{summary}</p> */}
<Button onClick={recipeInfoSend} btnText={'More Info'}/>
</div>
</div>
</div>
)
}
export default RecipeGridItem;
I then save the id state in my context provider
const [recipeId, setRecipeId] = useState('');
const addRecipeIDHandler = (id) => {
setRecipeId(id);
}
const recipeProviderValue = {
recipeId: recipeId,
};
Then I call the recipe ID down from context in my RecipeInfoPage.js file to make the necessary API call, this is the page the user is navigated to after clicking the button.
The data displays on the first page load, but if I refresh the page, the id value seems to get lost and all of my data from the api request is gone, leaving me with a blank page
import React, {useState, useContext, useEffect} from 'react';
import Container from '../Layout/Container';
import RecipeContext from '../../store/recipe-context';
const RecipeInfoPage = () => {
const recipeCtx = useContext(RecipeContext);
const recipeId = recipeCtx.recipeId;
const [recipeInfo, setRecipeInfo] = useState({});
const getRecipeInfo = () => {
fetch(`https://api.spoonacular.com/recipes/${recipeId}/information?apiKey=${process.env.REACT_APP_API_KEY}`)
.then(res => res.json())
.then(data => setRecipeInfo(data));
}
//Grid Recipes
useEffect(() => {
getRecipeInfo();
}, [recipeId]);
return (
<Container>
<h1>{recipeInfo.title}</h1>
<p dangerouslySetInnerHTML={{__html: recipeInfo.summary }}></p>
</Container>
)
}
export default RecipeInfoPage;
You should use params or query string instead of managing the id yourself.
history.push(`/info/${recipeId}`);
You will need to handle that route:
<Route path={'/info/:recipeId'}>
<YourComponent />
</Route>
And in YourComponent you can get the recipeId with useParams hook. And you can fetch the data with recipeId in useEffect
I am not very sure about this but isn't the command something along the lines of localStorage['myKey'] = 'somestring'; // only strings
I am trying to make a flashcard web app for language learning and/or rote learning. I have managed to show the first element of the array which contains the data that I'm fetching from the backend but I can't switch from the first element to the subsequent elements.
Here is my code in React:
// Decklist component that displays the flashcard
import { React, useEffect, useState, useContext } from "react";
import Card from "./Card";
import cardContext from "../store/cardContext";
const axios = require("axios");
export default function Decklist() {
//State for data fetched from db
const [data, setData] = useState([]);
//State for array element to be displayed from the "data" state
const [position, setPosition] = useState(0);
//function to change the array element to be displayed after user reads card
const setVisibility = () => {
setPosition(position++);
};
//function to change the difficulty of a card
const difficultyHandler = (difficulty, id) => {
console.log(difficulty);
setData(
data.map((ele) => {
if (ele.ID === id) {
return { ...ele, type: difficulty };
}
return ele;
})
);
};
//useEffect for fetching data from db
useEffect(() => {
axios
.get("/api/cards")
.then((res) => {
if (res.data) {
console.log(res.data);
setData(res.data.sort(() => (Math.random() > 0.5 ? 1 : -1)));
}
})
.catch((err) => {
console.log(err);
});
}, []);
return (
<cardContext.Provider
value={{ cardData: data, setDifficulty: difficultyHandler }}
>
{data.length && (
<Card
position={position}
// dataIndex={index}
visible={setVisibility}
id={data[position].ID}
front={data[position].Front}
back={data[position].Back}
/>
)}
</cardContext.Provider>
);
}
//Card component
import { React, useState, useEffect } from "react";
import Options from "./Options";
export default function Card(props) {
//State for showing or hiding the answer
const [reverse, setReverse] = useState(false);
const [display, setDisplay] = useState(true);
//function for showing the answer
const reversalHandler = () => {
setReverse(true);
};
return (
<div>
{reverse ? (
<div className="card">
{props.front} {props.back}
<button
onClick={() => {
props.visible();
}}
>
Next Card
</button>
</div>
) : (
<div className="card">{props.front}</div>
)}
<Options
visible={props.visible}
reverse={reversalHandler}
id={props.id}
/>
</div>
);
}
//Options Component
import { React, useContext, useState } from "react";
import cardContext from "../store/cardContext";
export default function Options(props) {
const ctx = useContext(cardContext);
const [display, setDisplay] = useState(true);
return (
<>
<div className={display ? "" : "inactive"}>
<button
onClick={() => {
setDisplay(false);
props.reverse();
ctx.setDifficulty("easy", props.id);
}}
>
Easy
</button>
<button
onClick={() => {
setDisplay(false);
props.reverse();
ctx.setDifficulty("medium", props.id);
}}
>
Medium
</button>
<button
onClick={() => {
setDisplay(false);
props.reverse();
ctx.setDifficulty("hard", props.id);
}}
>
Hard
</button>
</div>
</>
);
}
The setVisibility function in the Decklist component is working fine and setting the position state properly. However, I don't know how to re-render the Card component so that it acts on the position state that has changed.
One way to force a re-render of a component is to set its state to itself
onClick={() => {
props.visible();
setReverse(reverse);
}}
However this probably isn't your issue as components will automatically re-render when their state changes or a parent re-renders. This means that for some reason the Card component isn't actually changing the parent component.
I have right here a component that should simply render a list of items. Also, the component includes an input that filters the list of items. If there is no items, or if the items are being loaded it should display a message.
import { useState } from "react";
export const List = ({ loading, options }) => {
const _options = options ?? [];
const [renderedOptions, setRenderedOptions] = useState(_options);
const [inputValue, setInputValue] = useState("");
function handleChange(event) {
setInputValue(event.target.value);
const filteredOptions = _options.filter((option) =>
option.toLowerCase().includes(event.target.value.toLowerCase())
);
setRenderedOptions(filteredOptions);
}
return (
<div>
<input type="text" value={inputValue} onChange={handleChange} />
<ul>
{renderedOptions.length > 0 ? (
renderedOptions.map((option) => <li key={option}>{option}</li>)
) : loading ? (
<li>Loading...</li>
) : (
<li>Nothing to show</li>
)}
</ul>
</div>
);
};
In App.js, I did a setTimeout, to mock a fetch call. However, there is a problem. Although I'm setting the asyncOptions state to be the new list of items, in my <List /> component the options do not seem to display properly.
import { List } from "./List";
import { useState, useEffect } from "react";
const ITEMS = ["list_1", "list_2", "list_3", "list_4", "list_5"];
export default function App() {
const [asyncOptions, setAsyncOptions] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
const timeoutId = setTimeout(() => {
setIsLoading(false);
setAsyncOptions(ITEMS);
}, 2000);
return () => clearTimeout(timeoutId);
}, []);
return <List options={asyncOptions} loading={isLoading} />;
}
What is this happening and what is/are the solution(s)?
Sandbox code here: https://codesandbox.io/s/async-list-j97u32
The first time when list component gets rendered, renderedOptions is initialized with []
const [renderedOptions, setRenderedOptions] = useState(options);
But when the state inside the App component changes, it triggers a re-render and henceforth it triggers re-render of List Component. So since you are passing options as argument to useState u might feel it'll update the state automatically but that's not the case
Note -> useState doesn't take into consideration whatever you are passing as argument except for the first time the component loads
.So the useState will return back the initial State which is [] every time the component re-renders
So if you want to see changes you have to add useEffect inside the List component and trigger a state update every time options changes
Change your code too this,
import { useState } from "react";
export const List = ({ options, loading }) => {
console.log("Listt", options);
const [renderedOptions, setRenderedOptions] = useState([...options]);
const [inputValue, setInputValue] = useState("");
console.log(renderedOptions);
function handleChange(event) {
setInputValue(event.target.value);
const filteredOptions = options.filter((option) =>
option.toLowerCase().includes(event.target.value.toLowerCase())
);
setRenderedOptions(filteredOptions);
}
useEffect(() => {
setRenderedOptions(options)
} , [options])
return (
<div>
<input type="text" value={inputValue} onChange={handleChange} />
<ul>
{renderedOptions.length > 0 ? (
renderedOptions.map((option) => <li key={option}>{option}</li>)
) : loading ? (
<li>Loading...</li>
) : (
<li>Nothing to show</li>
)}
</ul>
</div>
);
};
Basically, in the beginning, the value of options in an empty array, and the value put in state is a copy of that so the component is not listening to changes on the prop.
For some reason, you have to use the useEffect hook to actively listen to changes in the prop. By using the hook, when the API call returns something, it will set the state.(BTW, if anyone knows what is going on tell us)
I would recommend moving the API call to the List component, it would better encapsulate the logic
import { useEffect, useState } from "react";
export const List = ({ loading, options }) => {
const [renderedOptions, setRenderedOptions] = useState(options);
const [inputValue, setInputValue] = useState("");
useEffect(() => {
setRenderedOptions(options);
}, [options]);
function handleChange(event) {
setInputValue(event.target.value);
const filteredOptions = options.filter((option) =>
option.toLowerCase().includes(event.target.value.toLowerCase())
);
setRenderedOptions(filteredOptions);
}
return (
<div>
<input type="text" value={inputValue} onChange={handleChange} />
<ul>
{renderedOptions.length > 0 ? (
renderedOptions.map((option) => <li key={option}>{option}</li>)
) : loading ? (
<li>Loading...</li>
) : (
<li>Nothing to show</li>
)}
</ul>
</div>
);
};
I want to add to the array a new object everytime I click at a card, but when I do so it changes the last key-pair to the new one and it doesnt add it. I chose this method of updating the state since I saw it is more popular than the one with the push.
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { AiOutlineHeart, AiFillHeart } from "react-icons/ai";
import styles from "./MovieCard.module.css";
const imagePrefixUrl = "http://image.tmdb.org/t/p/w500";
const MovieCard = (props) => {
const [items, setItems] = useState(
JSON.parse(localStorage.getItem("favorites")) || []
);
const [liked, setLiked] = useState(false);
const movie = props?.movie;
const addFavoriteHandler = (movie) => {
setItems((data) => [...data, movie]);
};
useEffect(() => {
localStorage.setItem("favorites", JSON.stringify(items));
}, [items]);
return (
<div className={styles.container}>
{liked ? (
<button className={styles.heartIcon} onClick={() => setLiked(false)}>
<AiFillHeart />
</button>
) : (
<button
className={styles.heartIcon}
onClick={() => addFavoriteHandler(movie)}
>
<AiOutlineHeart />
</button>
)}
<Link to={`/movie/${movie.id}`} title={movie?.title}>
<img src={`${imagePrefixUrl}${movie?.backdrop_path}`} />
<p>{movie?.title}</p>
</Link>
</div>
);
};
export default MovieCard;
I am assuming from the component name MovieCard, that your app would have multiple instances of this component under a parent component (assumed to be MovieCardList).
A solution to your issue would be to lift the state and addFavoriteHandler
const [items, setItems] = useState(
JSON.parse(localStorage.getItem("favorites")) || []
);
to the parent component MovieCardList and pass the handler addFavoriteHandler as a prop to each MovieCard.
This would ensure that you have a single point for updating your localStorage key favorites and it would not be overridden by new update.
The reason for the override issue you are experiencing is that each card has an instance of items and it does not fetch the latest value of favorites from the localStorage before updating it, meaning it would always override the favorites in localStorage as per the current code.
Creating a simple app using React and Redux.
The point is to get photos from the server, show them and if you click on the photo show modal window with bigger photo and comments.
The code for App component
import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import './App.scss'
import List from './components/list/List'
import Header from './components/header/Header'
import Footer from './components/footer/Footer'
import ModalContainer from './containers/ModalContainer'
import { getPhotos, openModal } from './redux/actions/actions'
const App = () => {
const { isFetching, error } = useSelector(({ photos }) => photos)
const photos = useSelector(({ photos }) => photos.photos)
const { isOpen } = useSelector(({ modal }) => modal)
const dispatch = useDispatch()
useEffect(() => {
dispatch(getPhotos())
}, [])
const getBigPhoto = (id) => {
dispatch(openModal(id))
}
return (
<div className="container">
<Header>Test App</Header>
<div className="list__content">
{isFetching
? <p>Loading...</p>
: error
? <p>{error}</p>
: photos.map(({ id, url }) => (
<List
key={id}
src={url}
onClick={() => getBigPhoto(id)}
/>
))
}
</div>
<Footer>© 2019-2020</Footer>
{isOpen && <ModalContainer />}
</div>
)
}
export default App
In this line I get photos only once to stop rerender if I refresh the page
useEffect(() => {
dispatch(getPhotos())
}, [])
When I click on the photo my modal opens and I want to stop rerendering all the components. For example for my header I use React.memo HOC like this
import React, { memo } from 'react'
import './Header.scss'
import PropTypes from 'prop-types'
const Header = memo(({ children }) => {
return <div className="header">{children}</div>
})
Header.propTypes = {
children: PropTypes.string,
}
Header.defaultProps = {
children: '',
}
export default Header
It works perfectly when I open and close my modal. Header and Footer are not rerendered. But List component is rerendered every time I open and close a modal window. It's happening because that prop onClick={() => getBigPhoto(id)} in List component creates a new anonymous function every time I click. As you know if your props changed, component is rerendered.
My question is how to avoid rerender of List component in my situation?
You can create a container for List that receives getBigPhoto and an id, create getBigPhoto with useCallback so the function doesn't change:
const ListContainer = React.memo(function ListContainer({
id,
src,
getBigPhoto,
}) {
return (
<List
key={id}
src={scr}
onClick={() => getBigPhoto(id)}
/>
);
});
const App = () => {
const { isFetching, error, photos } = useSelector(
({ photos }) => photos
);
const { isOpen } = useSelector(({ modal }) => modal);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getPhotos());
}, []);
//use callback so getBigPhoto doesn't change
const getBigPhoto = React.useCallback((id) => {
dispatch(openModal(id));
}, []);
return (
<div className="container">
<Header>Test App</Header>
<div className="list__content">
{isFetching ? (
<p>Loading...</p>
) : error ? (
<p>{error}</p>
) : (
photos.map(({ id, url }) => (
// render pure component ListContainer
<ListContainer
key={id}
src={url}
id={id}
getBigPhoto={getBigPhoto}
/>
))
)}
</div>
<Footer>© 2019-2020</Footer>
{isOpen && <ModalContainer />}
</div>
);
};