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.
Related
I am trying to update state of page index in index.js from component Pagination,
my index.js:
import useSWR from 'swr';
import { useState } from 'react';
const Index = ({ data }) => {
const initialStatePage = () => 1;
const [pageIndex, setPageIndex] = useState(initialStatePage);
const { data } = useSWR(`http://1.2.3.4/api/console?pagination[page]=${pageIndex}`, fetcher, {fallbackData: data});
return (
<>
<h1> {data} <h1/>
<Pagination pagenow={initialStatePage}/>
<>
);
};
export default Index;
my component:
import { useState } from 'react';
const Pagination = ({ pagenow }) => {
const [pageIndex, setPageIndex] = useState(pagenow);
return (
<>
<li>
<button onClick={() => setPageIndex(pageIndex - 1)}>
</button>
</li>
<button onClick={() => setPageIndex(pageIndex + 1)}>
</button>
</li>
</>
)
};
export default Pagination;
but after click, page index is not updating from my component
The state in your Pagination component will rerender the children element, not the whole page.
If you want it to rerender the whole Index page, pass your setPageIndex function to the component and use it to set the page index:
index.js
import useSWR from 'swr';
import { useState } from 'react';
const Index = ({ data }) => {
const initialStatePage = () => 1;
const [pageIndex, setPageIndex] = useState(initialStatePage);
const { data } = useSWR(`http://1.2.3.4/api/console?pagination[page]=${pageIndex}`, fetcher, {fallbackData: data});
return <>
<h1>{data}</h1>
<Pagination pagenow={initialStatePage} setPageIndex={setPageIndex} />
<>;
};
export default Index;
Pagination component file
import { useState } from 'react';
const Pagination = ({ pagenow: pageIndex, setPageIndex }) => {
return <>
<li>
<button onClick={() => setPageIndex(pageIndex - 1)}></button>
<button onClick={() => setPageIndex(pageIndex + 1)}></button>
</li>
</>;
};
export default Pagination;
I made a static webpage app that I have been slowly converting to React (MERN stack) to make it more dynamic/so I won't have to configure each and every HTML document. It's a product configurator that uses Google's model-viewer.
I'm fairly new to using a full-stack workflow but have found it pretty fun so far! I am having trouble however understanding on how to convert some of my vanilla JS to work within React. This particular script will change a source/3D model when a user clicks on a button. Below is a code snipit of what I have working currently on a static webpage.
import {useEffect, useState} from "react";
import {useSelector, useDispatch} from "react-redux";
// Actions
import {getProductDetails} from "../redux/actions/productActions";
const ProductScreen = ({match}) => {
const dispatch = useDispatch();
const [currentSrc, setCurrentSrc] = useState()
const [srcOptions, setSrcOptions] = useState()
const productDetails = useSelector((state) => state.getProductDetails);
const {loading, error, product} = productDetails;
useEffect(() => {
if (product && match.params.id !== product._id) {
dispatch(getProductDetails(match.params.id));
setCurrentSrc(product.src);
setSrcOptions(product.srcList);
}
}, [dispatch, match, product]);
return (
<div className="productcreen">
{loading ? (
<h2> Loading...</h2>) : error ? (
<h2>{error}</h2>) : (
<>
<div className='sizebuttons'>
{srcOptions.map((src) => (
<button onClick={() => setCurrentSrc(src)}>{src}{product.size}</button>
))}
{srcOptions.map((src) => (
<button onClick={() => setCurrentSrc(src)}>{src2}{product.size2}</button>
))}
{srcOptions.map((src) => (
<button onClick={() => setCurrentSrc(src)}>{src3}{product.size3}</button>
))}
</div>
<div className="productscreen__right">
<model-viewer id="model-viewer" src={currentSrc} alt={product.name} ar ar-modes="scene-viewer quick-look" ar-placement="floor" shadow-intensity="1" camera-controls min-camera-orbit={product.mincameraorbit} max-camera-orbit={product.maxcameraorbit} interaction-prompt="none">
<button slot="ar-button" className="ar-button">
View in your space
</button>
</model-viewer>
</div>
</> )} )};
Here is what the DB looks like:
The "product.size" is being pulled in from MongoDB, and I'm wondering if I could just swap models with: "product.src","product.src2","product.src3" (which is also defined in the DB already) I'm assuming I need to use useState in order to switch the source, but I am unsure. Any help would be greatly appreciated! If you'd like to see the static webpage of what I'm trying to accomplish, you can view it here if that helps at all.
Here is how the products are being exported in redux:
import * as actionTypes from '../constants/productConstants';
import axios from 'axios';
export const getProductDetails = (id) => async(dispatch) => {
try {dispatch({type: actionTypes.GET_PRODUCT_DETAILS_REQUEST});
const {data} = await axios.get(`/api/products/${id}`);
dispatch({
type: actionTypes.GET_PRODUCT_DETAILS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: actionTypes.GET_PRODUCT_DETAILS_FAIL,
payload: error.response && error.response.data.message ?
error.response.data.message :
error.message,
});
}
};
You can use the useState hook from React to create the state. After you fetch your product from the DB you can set the initial value with setCurrentSrc or if it's coming from props, you can set the initial value like this: const [currentSrc, setCurrentSrc] = useState(props.product.src).
Then change the src of your model-viewer to use the state value so it will automatically rerender if the state value changes. Lastly, add onClick handlers to some buttons with the setCurrentSrc function to change the state.
const ProductViewer = (props) => {
const [currentSrc, setCurrentSrc] = useState()
const [srcOptions, setSrcOptions] = useState()
const dispatch = useDispatch()
const { loading, error, product } = useSelector(
(state) => state.getProductDetails
)
useEffect(() => {
if (product && match.params.id !== product._id) {
dispatch(getProductDetails(match.params.id))
}
}, [dispatch, match, product])
// update src and srcOptions when product changes
useEffect(() => {
setCurrentSrc(product.src)
setSrcOptions(product.srcList)
}, [product])
return (
<div className="productscreen__right">
<model-viewer
id="model-viewer"
src={currentSrc}
alt={product.name}
ar
ar-modes="scene-viewer quick-look"
ar-placement="floor"
shadow-intensity="1"
camera-controls
min-camera-orbit={product.mincameraorbit}
max-camera-orbit={product.maxcameraorbit}
interaction-prompt="none"
>
<button slot="ar-button" className="ar-button">
View in your space
</button>
{/* add your switch buttons somewhere... */}
{/* this assumes you have a srcList, but this could also be hardcoded */}
{srcOptions.map((src) => (
<buttton onClick={() => setCurrentSrc(src)}>{src}</buttton>
))}
</model-viewer>
</div>
)
}
So I have a Context created with reducer. In reducer I have some logic, that in theory should work. I have Show Component that is iterating the data from data.js and has a button.I also have a windows Component that is iterating the data. Anyway the problem is that when I click on button in Show Component it should remove the item/id of data.js in Windows Component and in Show Component, but when I click on it nothing happens. I would be very grateful if someone could help me. Kind regards
App.js
const App =()=>{
const[isShowlOpen, setIsShowOpen]=React.useState(false)
const Show = useRef(null)
function openShow(){
setIsShowOpen(true)
}
function closeShowl(){
setIsShowOpen(false)
}
const handleShow =(e)=>{
if(show.current&& !showl.current.contains(e.target)){
closeShow()
}
}
useEffect(()=>{
document.addEventListener('click',handleShow)
return () =>{
document.removeEventListener('click', handleShow)
}
},[])
return (
<div>
<div ref={show}>
<img className='taskbar__iconsRight' onClick={() =>
setIsShowOpen(!isShowOpen)}
src="https://winaero.com/blog/wp-content/uploads/2017/07/Control-
-icon.png"/>
{isShowOpen ? <Show closeShow={closeShow} />: null}
</div>
)
}
```Context```
import React, { useState, useContext, useReducer, useEffect } from 'react'
import {windowsIcons} from './data'
import reducer from './reducer'
const AppContext = React.createContext()
const initialState = {
icons: windowsIcons
}
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
const remove = (id) => {
dispatch({ type: 'REMOVE', payload: id })
}
return (
<AppContext.Provider
value={{
...state,
remove,
}}
>
{children}
</AppContext.Provider>
)
}
export const useGlobalContext = () => {
return useContext(AppContext)
}
export { AppContext, AppProvider }
reducer.js
const reducer = (state, action) => {
if (action.type === 'REMOVE') {
return {
...state,
icons: state.icons.filter((windowsIcons) => windowsIcons.id !== action.payload),
}
}
}
export default reducer
``data.js```
export const windowsIcons =[
{
id:15,
url:"something/",
name:"yes",
img:"/images/icons/crud.png",
},
{
id:16,
url:"something/",
name:"nine",
img:"/images/icons/stermm.png",
},
{
id:17,
url:"domething/",
name:"ten",
img:"/images/icons/ll.png",
},
{
id:18,
url:"whatever",
name:"twenty",
img:"/images/icons/icons848.png",
},
{
id:19,
url:"hello",
name:"yeaa",
img:"/images/icons/icons8-96.png",
},
]
``` Show Component```
import React from 'react'
import { useGlobalContext } from '../../context'
import WindowsIcons from '../../WindowsIcons/WindowsIcons'
const Show = () => {
const { remove, } = useGlobalContext()
return (
<div className='control'>
{windowsIcons.map((unin)=>{
const { name, img, id} = unin
return (
<li className='control' key ={id}>
<div className='img__text'>
<img className='control__Img' src={img} />
<h4 className='control__name'>{name}</h4>
</div>
<button className='unin__button' onClick={() => remove(id)} >remove</button>
</li> )
</div>
)
}
export default Show
import React from 'react'
import {windowsIcons} from "../data"
import './WindowsIcons.css'
const WindowsIcons = ({id, url, img, name}) => {
return (
<>
{windowsIcons.map((icons)=>{
const {id, name , img ,url} =icons
return(
<div className='windows__icon' >
<li className='windows__list' key={id}>
<a href={url}>
<img className='windows__image' src={img}/>
<h4 className='windows__text'>{name}</h4>
</a>
</li>
</div>
)
})}
</>
)
}
Issue
In the reducer you are setting the initial state to your data list.
This is all correct.
However, then in your Show component you are directly importing windowsIcons and looping over it to render. So you are no longer looping over the state the reducer is handling. If the state changes, you won't see it.
Solution
In your Show component instead loop over the state that you have in the reducer:
const { remove, icons } = useGlobalContext()
{icons.map((unin) => {
// Render stuff
}
Now if you click remove it will modify the internal state and the icons variable will get updated.
Codesandbox working example
I'm new to React. I'm attempting to add an onClick event to a div element that will remove a className from another element. These elements are part of a loop in a map. I am attempting to use the useRef hook for this. I specifically don't want to toggle classNames, I want to remove it, and that's it. Then add it back with another onclick event from another element. This requirement is specific to my application. My current code removes the className from the last element, not the one I am targeting. Thanks in advance for any help!
import React, { useState, useEffect, useRef } from "react";
import axios from "axios";
import "./styles.css";
export default function App() {
const [kitchenItems, setkitchenItems] = useState([]);
useEffect(() => {
axios.get("./data.json").then((res) => {
setkitchenItems(res.data.kitchen);
});
}, []);
const navRef = React.useRef(null);
const onRemoveClick = (e) => {
navRef.current.classList.remove("red");
};
return (
<main>
{kitchenItems.map((item, index) => (
<div key={index} className="item">
<div onClick={onRemoveClick}>
<h2>{item.name}</h2>
<p ref={navRef} className="red">
{item.text}
</p>
</div>
</div>
))}
</main>
);
}
Here it is in CodeSandbox:
https://codesandbox.io/s/lively-moon-d7mm3
Save the indicator of whether an item should have the class or not into the kitchenItems state. Remove the ref.
useEffect(() => {
axios.get("./data.json").then((res) => {
setkitchenItems(res.data.kitchen.map(item => ({ ...item, red: true })));
});
}, []);
const onRemoveClick = (i) => {
setkitchenItems(
kitchenItems.map((item, j) => j !== i ? item : ({ ...item, red: false }))
);
};
<div onClick={() => onRemoveClick(i)}>
<h2>{item.name}</h2>
<p className={item.red ? 'red' : ''}>
The way you are approaching this is the way it's done with jQuery, that you change the dom elements. In React you would instead update the state and let the component render. I have added a new property on your objects but you may have another list or some other logic if you wish.
import React, { useState, useEffect, useRef } from "react";
import axios from "axios";
import "./styles.css";
export default function App() {
const [kitchenItems, setkitchenItems] = useState([]);
useEffect(() => {
axios.get("./data.json").then((res) => {
setkitchenItems(res.data.kitchen);
});
}, []);
const onRemoveClick = (index) => {
debugger;
const tmpItems = [...kitchenItems];
tmpItems[index].isRed = !tmpItems[index].isRed;
setkitchenItems(tmpItems);
};
return (
<main>
{kitchenItems.map((item, index) => (
<div key={index} className="item">
<div onClick={() => onRemoveClick(index)}>
<h2>{item.name}</h2>
<p className={item.isRed ? "red" : ""}>{item.text}</p>
</div>
</div>
))}
</main>
);
}
In CodeSandbox: https://codesandbox.io/s/keen-kirch-55whe?file=/src/App.js
Why not just use pureJS?
const onRemoveClick = (e) => {
var target = e.target;
if (target) {
if (target.classList && target.classList.contains("red")) {
target.classList.remove("red");
} else {
target.classList.add("red");
}
}
};
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>
);
};