Writing a simple frontend pagination, with a next and a back button. When I console.log(state) may state seems to be changing according to my plan when clicking the buttons. However the webpage displays the state data from one page back.
import React, { useEffect, useState } from "react";
import { useStateValue } from "../state";
import getChar from "../api/index";
import Spinner from "../components/loading"
export default function Characters(props) {
// access state and dispatch functions here
const [state, dispatch] = useStateValue();
const [loading, setLoading] = useState(true);
const [index, setIndex] = useState(0);
let [page, setPage] = useState(1);
const apiCall = async (num) => {
const first = (4 * num) - 4;
const last = (4 * num) - 1;
const res = await getChar.getCharacters();
const display = res.results.filter((char, index) => {
return (index >= first && index <= last);
})
await dispatch({
type: 'GET_CHARACTERS',
data: display
});
setLoading(false)
setIndex(res.results.length)
}
// move to next page
const next = () => {
const newPage = page + 1;
const maxPages = Math.ceil(index / 4);
if (newPage > maxPages) {
setPage(maxPages)
apiCall(maxPages)
} else {
setPage(newPage)
apiCall(newPage)
}
}
// move back a page
const back = () => {
const newPage = page - 1;
if (newPage === 0) {
setPage(1)
apiCall(1)
} else {
setPage(newPage)
apiCall(newPage)
}
}
useEffect(() => {
apiCall(1)
}, []);
if (loading) {
return <div><Spinner /></div>
}
return (
<div>
<h1>Characters Page</h1>
{
state.characters.map((char, index)=> (
(char.starships.length === 0) ?
<div className="List" key={index}>
<h3 className="character">
{char.name}
</h3>
</div>
:
<div className="List" key={index}>
<h3 className="character">
{char.name} - pilot
</h3>
</div>
))
}
<button onClick={back}>back</button>
<button onClick={next}>next</button>
</div>
);
}
export default function reducer(state, action) {
switch (action.type) {
case "GET_CHARACTERS":
state.characters = action.data;
return state; default: return state;
}
}
Shows the previous states data before click until the last page in the pagination. Will not show the last bit of paginated data until click the back button viceversa for the first bit of data.
The react can't detect a change in a mutable object. A new state object should be returned in every reducer.
case "GET_CHARACTERS":
state.characters = action.data;
return {...state};
or
case "GET_CHARACTERS":
return {...state, characters: action.data};
Some useful information:
This is a note on state and setState in the react documentation. using-state-correctly
This is mentioned in the introduction of the useState of the react, how does the react compare the two states before and after. Bailing out of a state update
This is what was mentioned when redux introduced the reducer. We don't mutate the state.
this is a demo of mutable object.
var a= {name:1}
var b = a;
b.name=2
var result = Object.is(a,b)
console.log(result) // true
Related
I'm building an app with React and Firebase Realtime Database. Objects are added to an array and sent to the database.
The arrays are updated in React and the result is sent to the database.
The functionality to remove items/objects from the list works fine when there are more than one (i.e. button clicked, database, DOM and state updated immediately).
However, whenever there's one item left and you click its delete button, it's deleted from the database but the state and React DOM aren't updated - you have to refresh the page for it to be removed.
I've tried using different methods to update the database in case it triggered a different response but that didn't work - any ideas would be greatly appreciated:
import React, {useState, useEffect} from 'react'
import { Button } from "react-bootstrap";
import Exercise from "./Exercise";
import AddNewWorkout from "./AddNewWorkout";
import { v4 as uuidv4 } from "uuid";
import WorkoutComponent from './WorkoutComponent';
import AddNewExercise from "./AddNewExercise"
import { database, set, ref, onValue, update } from "../firebase"
const Dashboard = ({user}) => {
const [selectedWorkout, setSelectedWorkout] = useState();
const [workouts, setWorkouts] = useState([])
const [creatingNewWorkout, setCreatingNewWorkout] = useState(false);
const [addingNewExercise, setAddingNewExercise] = useState(false)
function selectWorkout(number) {
const selection = [...workouts].filter(workout => number == workout.id);
setSelectedWorkout(selection[0])
}
function toggleNewWorkoutStatus(e) {
e.preventDefault()
setCreatingNewWorkout(creatingNewWorkout => !creatingNewWorkout)
}
function toggleNewExerciseStatus() {
setAddingNewExercise(addingNewExercise => !addingNewExercise)
}
function writeData() {
const newWorkouts = [...workouts]
const workoutTitle = document.getElementById("workoutTitle").value || new Date(Date.now()).toString()
const workoutDate = document.getElementById("workoutDate").value;
newWorkouts.push({
id: uuidv4(),
title: workoutTitle,
date: workoutDate,
exercises: []
})
set(ref(database, `${user.uid}/workouts/`), newWorkouts )
}
function addWorkoutToListDB(e) {
e.preventDefault();
writeData(user.uid)
}
function removeWorkoutFromList(id) {
const newWorkouts = [...workouts].filter(workout => id !== workout.id);
update(ref(database, `${user.uid}`), {"workouts": newWorkouts} )
}
function addExerciseToWorkout(e) {
e.preventDefault();
if (selectedWorkout === undefined) {
alert("No workout selected")
return
}
const newWorkouts = [...workouts]
const exerciseID = uuidv4();
const exerciseName = document.getElementById("exerciseName").value
const exerciseSets = document.getElementById("exerciseSets").value
const exerciseReps = document.getElementById("exerciseReps").value
const exerciseWeight = document.getElementById("exerciseWeight").value
const exercisetTarget = document.getElementById("exercisetTarget").checked
const exerciseNotes = document.getElementById("exerciseNotes").value;
const newExercise = {
id: exerciseID,
name: exerciseName,
sets: exerciseSets,
reps: exerciseReps,
weight: `${exerciseWeight}kg`,
target: exercisetTarget,
notes: exerciseNotes,
}
for (let key of newWorkouts) {
if (key.id === selectedWorkout.id) {
if (key.exercises) {
key.exercises.push(newExercise)
} else {
key.exercises = [newExercise]
}
}
}
update(ref(database, `${user.uid}`), {"workouts": newWorkouts} )
}
function removeExerciseFromWorkout(id) {
const newWorkouts = [...workouts];
for (let workout of newWorkouts) {
if(selectedWorkout.id === workout.id) {
if (!workout.exercises) {return}
workout.exercises = workout.exercises.filter(exercise => exercise.id !== id)
}
}
const newSelectedWorkout = {...selectedWorkout}
newSelectedWorkout.exercises = newSelectedWorkout.exercises.filter(exercise => exercise.id !== id)
setSelectedWorkout(newSelectedWorkout)
update(ref(database, `${user.uid}`), {"workouts": newWorkouts} )
}
useEffect(() => {
function getWorkoutData() {
const dbRef = ref(database, `${user.uid}`);
onValue(dbRef, snapshot => {
if (snapshot.val()) {
console.log(snapshot.val().workouts)
setWorkouts(workouts => workouts = snapshot.val().workouts)
}
}
)
}
getWorkoutData()
},[])
return (
<div>
{creatingNewWorkout && <AddNewWorkout addWorkoutToListDB={addWorkoutToListDB} toggleNewWorkoutStatus={toggleNewWorkoutStatus} /> }
<div id="workoutDiv">
<h2>Workouts</h2><p>{selectedWorkout ? selectedWorkout.title : "No workout selected"}</p>
<Button type="button" onClick={toggleNewWorkoutStatus} className="btn btn-primary">Add New Workout</Button>
{workouts && workouts.map(workout => <WorkoutComponent key={workout.id} removeWorkoutFromList={removeWorkoutFromList} selectWorkout={selectWorkout} workout={workout}/> )}
</div>
<div>
<h2>Exercise</h2>
{addingNewExercise && <AddNewExercise selectedWorkout={selectedWorkout} addExerciseToWorkout={addExerciseToWorkout} toggleNewExerciseStatus={toggleNewExerciseStatus}/> }
<Button type="button" onClick={toggleNewExerciseStatus} className="btn btn-primary">Add New Exercise</Button>
{selectedWorkout && selectedWorkout.exercises && selectedWorkout.exercises.map(exercise => <Exercise removeExerciseFromWorkout={removeExerciseFromWorkout} key={exercise.id} exercise={exercise}/>)}
</div>
</div>
)
}
export default Dashboard
If it helps, the data flow I'm working to is:
New array copied from state
New array updated as necessary
New array sent to database
Database listener triggers download of new array
New array saved to state
I have tried to use different methods (set, update and remove) in case that triggered the onValue function.
I have also tried to send null values and deleting empty nodes if the array that will be sent to the db is empty.
The above methods didn't have any impact, there was still a problem with the last array element that was only resolved by refreshing the browser.
I have tried to remove the array dependency and add the workout state as a dependency, resulting in the following error: "Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render."
I think I understand where the issue was:
In the useEffect call, I set up the state to only be updated if the value in returned from the database was null (to prevent an error I ran into). However, this meant that state wasn't being updated at all when I deleted the last item from the array.
I appear to have fixed this by adding an else clause.
useEffect(() => {
function getWorkoutData() {
const dbRef = ref(database, `${user.uid}`);
onValue(dbRef, snapshot => {
if (snapshot.val()) {
console.log(snapshot.val().workouts)
setWorkouts(workouts => workouts = snapshot.val().workouts)
} else {
setWorkouts(workouts => workouts = [])
}
}
)
}
getWorkoutData()
},[])
`````
What I want is to paginate my data but the problem is when I'm searching for specific data if I'm on page 3 the result shows on page 1 always and I can't see anything because I was on page no 3. I want to go to page 1 automatically when I'm searching for something. Also when I press the next button if there is no data at all it still increases the page number.
Here is my code:
import { React, useState, useEffect } from "react";
import UpdateDialogue from "./UpdateDialogue";
function List(props) {
const API_URL = "http://dummy.restapiexample.com/api/v1/employees";
const [EmployeeData, setEmployeeData] = useState([]);
const [pageNumber, setPageNumber] = useState(1);
const [postNumber] = useState(8);
const currentPageNumber = pageNumber * postNumber - postNumber;
const handlePrev = () => {
if (pageNumber === 1) return;
setPageNumber(pageNumber - 1);
};
const handleNext = () => {
setPageNumber(pageNumber + 1);
};
useEffect(() => {
fetch(API_URL)
.then((response) => response.json())
.then((response) => {
setEmployeeData(response.data);
})
.catch((err) => {
console.error(err);
});
}, []);
const filteredData = EmployeeData.filter((el) => {
if (props.input === "") {
return el;
} else {
return el.employee_name.toLowerCase().includes(props.input)
}
});
const paginatedData = filteredData.splice(currentPageNumber, postNumber);
return (
<>
<ul>
{paginatedData.map((user) => (
<UpdateDialogue user={user} key={user.id} />
))}
</ul>
<div>Page {pageNumber} </div>
<div>
<button style={{marginRight:10}} onClick={handlePrev}>prev</button>
<button onClick={handleNext}>next</button>
</div>
</>
);
}
export default List;
Maybe with a useEffect on your input:
useEffect(() => {
if (props.input) {
setPageNumber(1);
}
}, [props.input]);
That way, whenever your input changes, your page number is set to 1.
I have a functional component with a useState hook. Its values are coming from my redux store and I want to update the state with the are store state every time a dispatch an action.
Right now I have hardcoded an array that the useState starts with. I want to be able to push in new elements in the array via redux and have react re-render the new content.
See code below:
import React, { useState } from "react";
import "./style.scss";
import { FormEquation } from "../calc/interfaces/form";
import { FlowrateCalc } from "../calc/calculators/FlowrateCalc";
import { useSelector } from "react-redux";
import { RootState } from "../state/reducers";
import { ValveKvsCalc } from "../calc/calculators/ValveKvsCalc";
function Calculator() {
const state = useSelector((state: RootState) => state.calc);
// const state = [
// {
// ...FlowrateCalc,
// priorityList: FlowrateCalc.inputs.map((input) => input.name),
// },
// {
// ...ValveKvsCalc,
// priorityList: ValveKvsCalc.inputs.map((input) => input.name),
// },
// ];
// Usestate is run once after render and never again. How do I update this state whenever new content arrived from "useSelector"??
const [formsEQ, setformsEQ] = useState<FormEquation[]>([...state]);
const inputsHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
// Copy form and get index of affected form
const formCopy = formsEQ.slice();
const [formName, inputFieldName] = e.target.name.split("-");
const formIndex = formsEQ.findIndex((formEQ) => formEQ.name === formName);
if (formIndex === -1) return;
// if anything other than a number or dot inputted, then return
// meTODO: if added number then trying to delete all numbers will stop!
const isInputNum = e.target.value.match(/[0-9]*\.?[0-9]*/);
if (!isInputNum || isInputNum[0] === "") return;
// Update priority list to calculate the last updated input
formCopy[formIndex].priorityList = formCopy[formIndex].priorityList.sort((a, b) => {
if (a === inputFieldName) return 1;
if (b === inputFieldName) return -1;
else return 0;
});
// Update selected input field
formCopy[formIndex].inputs = formCopy[formIndex].inputs.map((input) => {
if (input.name === inputFieldName) {
input.value = e.target.value;
}
return input;
});
// If more than two inputs empty do not calculate
const emptyInputs = formCopy[formIndex].inputs.reduce(
(acc, nV) => (nV.value === "" ? (acc += 1) : acc),
0
);
// Calculate the last edited input field
formCopy[formIndex].inputs = formCopy[formIndex].inputs.map((input) => {
if (input.name === formCopy[formIndex].priorityList[0] && emptyInputs <= 1) {
const calculatedValue = formCopy[formIndex].calculate(formCopy[formIndex].priorityList[0]);
input.value = calculatedValue;
}
return input;
});
// Final set hook, now with calculated value
setformsEQ([...formCopy]);
};
const formInputs = formsEQ.map((formEQ) => {
return (
<form className="form" key={formEQ.name}>
{formEQ.inputs?.map((formInput) => {
return (
<div className="form__input" key={formInput.name}>
<label>{formInput.label}: </label>
<input
name={`${formEQ.name}-${formInput.name}`}
onChange={inputsHandler}
placeholder={`${formInput.label} (${formInput.selectedUnit})`}
value={formInput.value}
/>
</div>
);
})}
</form>
);
});
return <div>{formInputs}</div>;
}
export default Calculator;
To whomever is reading this and is a rookie in react like me.
The solution for me was to use useEffect hook; And whenever useSelector updates the state constant, the useEffect hook will use the useState set function to update the state.
See added code below that fixed my problem:
useEffect(() => {
setformsEQ([...state])
}, [state])
I wonder if how to set multiple useEffect hooks in Reactjs to prevent duplication of queries.
I have DocumentList which gets string variable "query" from parent and variable "page".
First is used to get document list from remote api. Second one - to set pagination value.
These variables aree used in fetch query.
Please, look at the code:
import React, { useState, useEffect } from "react"
import axios from 'axios'
import { Link, useLocation } from "react-router-dom";
export default function DocumentList({query}) {
const [page, setPage] = useState(1);
useEffect( () => {
results();
}, [query, page])
let ajaxRequest = null;
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [items, setItems] = useState([]);
const dataUrl = 'https://api/test';
const results = () => {
...
}
function List() {
...
}
const handlePrevPage = () => {
if (page > 1) {
setPage(page - 1);
}
}
const handleNextPage = () => {
setPage(page + 1);
}
function Pagination() {
return (
<button onClick={handlePrevPage}>
Prev
</button>
<span className="block py-1 px-2 font-bold text-white">{page}</span>
<button onClick={handleNextPage} >
Next
</button>
</div>
</div>
);
}
...
}
And the question is how to combine these variables setting to prevent duplication of request?
If i set only one of them - or "query", or "page" all working good. But how can I reset "page" for new query?
Thanks!
You can use 2 useEffects like below
const handleNextPage = () => {
setPage(page + 1);
setTrigger(prev => !prev)
}
const [trigger, setTrigger] = useState(false);
useEffect( () => {
setPage(1)
setTrigger(prev => !prev)
}, [query])
useEffect( () => {
results();
}, [trigger])
Please note trigger ensures triggering results when a new query passed while the page state is already 1
I have this code:
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [state, setState] = useState(0);
const add = () => {
setState(state + 1);
localStorage.setItem("data", JSON.stringify([state].push(state)));
};
return (
<div className="App">
<button onClick={add}>click</button>
</div>
);
}
I try to add in localStorage numbers clicking on the button.
At the end i want to get in local storage something like this: [1,2,3,4,5,6], depending how many times user click on button.
Now i don't get the expected value.
Question: How to get what i described?
demo:https://codesandbox.io/s/youthful-grothendieck-dwewm?file=/src/App.js:0-359
What about this?
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [state, setState] = useState([]);
const add = () => {
const _numbers = [...state];
_numbers.push(_numbers.length + 1);
setState(_numbers);
localStorage.setItem("data", JSON.stringify(_numbers));
};
return (
<div className="App">
<button onClick={add}>click</button>
</div>
);
}
it clones (not nested objects, beware!) the empty array (state)
it adds to the array, the length of the array + 1, so you get 1, 2, 3, 4, 5, etc..
it sets the state with that value and stringifies that value to localstorage
other possibility with prevState in a setState call, does basically the same but uses a reference variable
const [state, setState] = useState([]);
const add = () => {
setState(prevState => {
prevState.push(prevState.length + 1);
localStorage.setItem("data", JSON.stringify(prevState));
return prevState;
});
}
First try getting current stored values from localStorage, push the new state and then set in localStorage with updated array. (First time when value not available in LS, default the value to []).
export default function App2() {
const [state, setState] = useState(0);
const add = () => {
const newState = state + 1;
setState(newState);
// Get the currently stored clicks from local storage
const clicks = JSON.parse(localStorage.getItem("data2")) ?? [];
clicks.push(newState);
localStorage.setItem("data2", JSON.stringify(clicks));
};
return (
<div className="App">
<button onClick={add}>click2</button>
</div>
);
}
First: your state should be const [state, setState] = useState([]);
Second:
Array.push returns the new length property of the object upon which the method was called. You want Array.concat method is used to merge two or more arrays returns the new constructed Array.
So this should work:
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [state, setState] = useState([]);
const add = () => {
setState(prevState => {
const newState = prevState.concat(prevState.length + 1);
localStorage.setItem("data", JSON.stringify(newState));
return newState;
});
};
return (
<div className="App">
<button onClick={add}>click</button>
</div>
);
}