Conflicts between useEffect in react - reactjs

I have to create component which fetch data with pagination and filters.
Filters are passed by props and if they changed, component should reset data and fetch it from page 0.
I have this:
const PaginationComponent = ({minPrice, maxPrice}) => {
const[page, setPage] = useState(null);
const[items, setItems] = useState([]);
const fetchMore = useCallback(() => {
setPage(prevState => prevState + 1);
}, []);
useEffect(() => {
if (page === null) {
setPage(0);
setItems([]);
} else {
get(page, minPrice, maxPrice)
.then(response => setItems(response));
}
}, [page, minPrice, maxPrice]);
useEffect(() => {
setPage(null);
},[minPrice, maxPrice]);
};
.. and there is a problem, because first useEffect depends on props, because I use them to filtering data and in second one I want to reset component. And as a result after changing props both useEffects run.
I don't have more ideas how to do it correctly.

In general the key here is to move page state up to the parent component and change the page to 0 whenever you change your filters. You can do it either with useState, or with useReducer.
The reason why it works with useState (i.e. there's only one rerender) is because React batches state changes in event handlers, if it didn't, you'd still end up with two API calls. CodeSandbox
const PaginationComponent = ({ page, minPrice, maxPrice, setPage }) => {
const [items, setItems] = useState([]);
useEffect(() => {
get(page, minPrice, maxPrice).then(response => setItems(response));
}, [page, minPrice, maxPrice]);
return (
<div>
{items.map(item => (
<div key={item.id}>
{item.id}, {item.name}, ${item.price}
</div>
))}
<div>Page: {page}</div>
<button onClick={() => setPage(v => v - 1)}>back</button>
<button onClick={() => setPage(v => v + 1)}>next</button>
</div>
);
};
const App = () => {
const [page, setPage] = useState(0);
const [minPrice, setMinPrice] = useState(25);
const [maxPrice, setMaxPrice] = useState(50);
return (
<div>
<div>
<label>Min price:</label>
<input
value={minPrice}
onChange={event => {
const { value } = event.target;
setMinPrice(parseInt(value, 10));
setPage(0);
}}
/>
</div>
<div>
<label>Max price:</label>
<input
value={maxPrice}
onChange={event => {
const { value } = event.target;
setMaxPrice(parseInt(value, 10));
setPage(0);
}}
/>
</div>
<PaginationComponent minPrice={minPrice} maxPrice={maxPrice} page={page} setPage={setPage} />
</div>
);
};
export default App;
The other solution is to use useReducer, which is more transparent, but also, as usual with reducers, a bit heavy on the boilerplate. This example behaves a bit differently, because there is a "set filters" button that makes the change to the state that is passed to the pagination component, a bit more "real life" scenario IMO. CodeSandbox
const PaginationComponent = ({ tableConfig, setPage }) => {
const [items, setItems] = useState([]);
useEffect(() => {
const { page, minPrice, maxPrice } = tableConfig;
get(page, minPrice, maxPrice).then(response => setItems(response));
}, [tableConfig]);
return (
<div>
{items.map(item => (
<div key={item.id}>
{item.id}, {item.name}, ${item.price}
</div>
))}
<div>Page: {tableConfig.page}</div>
<button onClick={() => setPage(v => v - 1)}>back</button>
<button onClick={() => setPage(v => v + 1)}>next</button>
</div>
);
};
const tableStateReducer = (state, action) => {
if (action.type === "setPage") {
return { ...state, page: action.page };
}
if (action.type === "setFilters") {
return { page: 0, minPrice: action.minPrice, maxPrice: action.maxPrice };
}
return state;
};
const App = () => {
const [tableState, dispatch] = useReducer(tableStateReducer, {
page: 0,
minPrice: 25,
maxPrice: 50
});
const [minPrice, setMinPrice] = useState(25);
const [maxPrice, setMaxPrice] = useState(50);
const setPage = useCallback(
page => {
if (typeof page === "function") {
dispatch({ type: "setPage", page: page(tableState.page) });
} else {
dispatch({ type: "setPage", page });
}
},
[tableState]
);
return (
<div>
<div>
<label>Min price:</label>
<input
value={minPrice}
onChange={event => {
const { value } = event.target;
setMinPrice(parseInt(value, 10));
}}
/>
</div>
<div>
<label>Max price:</label>
<input
value={maxPrice}
onChange={event => {
const { value } = event.target;
setMaxPrice(parseInt(value, 10));
}}
/>
</div>
<button
onClick={() => {
dispatch({ type: "setFilters", minPrice, maxPrice });
}}
>
Set filters
</button>
<PaginationComponent tableConfig={tableState} setPage={setPage} />
</div>
);
};
export default App;

You can use following
const fetchData = () => {
get(page, minPrice, maxPrice)
.then(response => setItems(response));
}
// Whenever page updated fetch new data
useEffect(() => {
fetchData();
}, [page]);
// Whenever filter updated reseting page
useEffect(() => {
const prevPage = page;
setPage(0);
if(prevPage === 0 ) {
fetchData();
}
},[minPrice, maxPrice]);

Related

Todo fetcher and Filter

I'm having difficulty in filtering the my todo-data according to the checkbox value. By default checkbox is checked which shows all data from the response. when Show Completed is checked alone it should only display the completed items, similarly for Show Incompleted checkbox.
export const Todo = () => {
const [todo, setTodo] = useState([]);
const [loading, setloading] = useState(false);
//fetched data
async function fetchData() {
setloading(true);
const data = await fetch("https://jsonplaceholder.typicode.com/todos ");
let res = await data.json();
res = res.splice(0, 20);
setTodo(res);
setloading(false);
}
useEffect(() => {
fetchData();
}, []);
//handle onChange
const compCheck = (e) => {
};
const InCompCheck = (e) => {
};
return (
<>
{loading && (
<h1>
<Loader />
</h1>
)}
{!loading && (
<>
<TodoItems todo={todo} loading={loading} />
<div id="filter-holder">
<label>Show Completed</label>
<input
id="completed-checkbox"
type="checkbox"
onChange={compCheck}
checked //by default should be checked to show complete list
/>
<br />
<label>Show Incompleted</label>
<input
id="incompleted-checkbox"
type="checkbox"
onChange={InCompCheck}
checked
/>
</div>
</>
)}
</>
);
};
this is sandbox link: https://codesandbox.io/s/todo-fetcher-and-filter-rci40?file=/src/Todo.js
Create a state for two checkboxes, update the state on change listener and then filter the todo array based on the checkbox state.
export const Todo = () => {
const [todo, setTodo] = useState([]);
const [loading, setloading] = useState(false);
const [checked, setChecked] = useState({ complete: true, incomplete: true });
async function fetchData() {
setloading(true);
const data = await fetch("https://jsonplaceholder.typicode.com/todos ");
let res = await data.json();
res = res.splice(0, 20);
setTodo(res);
setloading(false);
}
useEffect(() => {
fetchData();
}, []);
const getFilteredTodo = () => {
//if both are unchecked show nothing
if (!checked.complete && !checked.incomplete) return [];
return todo.filter((obj) => {
//if both are checked show all todo
if (checked.complete && checked.incomplete) return obj;
//filter objects based on the `Show completed` checkbox state
return checked.complete ? obj.completed: !obj.completed
});
};
const compCheck = (e) => {
setChecked((curr) => ({ ...curr, complete: e.target.checked }));
};
const InCompCheck = (e) => {
setChecked((curr) => ({ ...curr, incomplete: e.target.checked }));
};
return (
<>
{loading && (
<h1>
<Loader />
</h1>
)}
{!loading && (
<>
<TodoItems todo={getFilteredTodo()} loading={loading} />
<div id="filter-holder">
<label>Show Completed</label>
<input
id="completed-checkbox"
type="checkbox"
onChange={compCheck}
checked={checked.complete}
/>
<br />
<label>Show Incompleted</label>
<input
id="incompleted-checkbox"
type="checkbox"
onChange={InCompCheck}
checked={checked.incomplete}
/>
</div>
</>
)}
</>
);
};
First thing you need to do is controll your checkbox value and store in state.
Then write a filter function that filters the original TODO list. Then call the function in the component so that it executes itself with every state update and then loop through its result to render it.
const filterTodos = (todos, showCompleted, showIncompleted) => {
let filteredList = todos;
if (!showCompleted)
filteredList = filteredList.filter((todo) => !todo.completed);
if (!showIncompleted)
filteredList = filteredList.filter((todo) => todo.completed);
return filteredList;
};
const TodoList = () => {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
//Set the initial values in state not in the input props
const [showCompleted, setShowCompleted] = useState(true);
const [showIncompleted, setShowIncompleted] = useState(true);
const onCompletedChangeHandler = (e) => {
setShowCompleted(e.target.checked);
};
const onInCompletedChangeHandler = (e) => {
setShowIncompleted(e.target.checked);
};
useEffect(() => {
async function fetchData() {
setLoading(true);
const data = await fetch("https://jsonplaceholder.typicode.com/todos ");
let res = await data.json();
res = res.splice(0, 20);
setTodos(res);
setLoading(false);
}
fetchData();
}, []);
return (
<>
{loading && (
<h1>
<Loader />
</h1>
)}
{!loading && (
<>
<TodoItems
todo={filterTodos(todos, showCompleted, showIncompleted)}
loading={loading}
/>
<div id="filter-holder">
<label>Show Completed</label>
<input
id="completed-checkbox"
type="checkbox"
onChange={onCompletedChangeHandler}
value={showCompleted}
/>
<br />
<label>Show Incompleted</label>
<input
id="incompleted-checkbox"
type="checkbox"
onChange={onInCompletedChangeHandler}
value={showIncompleted}
/>
</div>
</>
)}
</>
);
};

On click of button not filtering array like intended

Can someone explain to me why the filter method isn't working as intended? I am trying to update my array of objects with filtered results based on the first name.
export default function App() {
const { data } = useDataFetcher();
const searchArray = data;
const [inputField, setInputField] = useState("");
const searchUser = () => {
searchArray.filter((el) => el.firstName.includes(inputField));
};
useEffect(() => {
searchUser();
}, [inputField]);
return (
<div className="App">
<h1>Xaxis Frontend Interview</h1>
<input onChange={(e) => setInputField(e.target.value)} />
<button onClick={(e) => searchUser()}> Search </button>
{searchArray.map((userData, i) => (
<Users
key={i}
firstName={userData.firstName}
lastName={userData.lastName}
email={userData.email}
bio={userData.bio}
/>
))}
</div>
);
}
You need to create a state which will contain the list of the users. Then, you want to update this state when inputField state changes.
export default function App() {
const { data } = useDataFetcher();
const searchArray = data;
const [users, setUsers] = useState([])
const [inputField, setInputField] = useState("");
useEffect(() => {
setUsers(data);
}, [data]);
useEffect(() => {
const filteredArray = users.filter((el) => el.firstName.includes(inputField));
setUsers(filteredArray)
}, [inputField]);
return (
<div className="App">
<h1>Xaxis Frontend Interview</h1>
<input onChange={(e) => setInputField(e.target.value)} />
{users.map((userData, i) => (
<Users
key={i}
firstName={userData.firstName}
lastName={userData.lastName}
email={userData.email}
bio={userData.bio}
/>
))}
</div>
);
}
Or if you want to update the list of users by clicking a button:
export default function App() {
const { data } = useDataFetcher();
const searchArray = data;
const [users, setUsers] = useState([])
const [inputField, setInputField] = useState("");
useEffect(() => {
setUsers(data);
}, [data]);
const searchUser = () => {
const filteredArray = users.filter((el) => el.firstName.includes(inputField));
setUsers(filteredArray)
};
return (
<div className="App">
<h1>Xaxis Frontend Interview</h1>
<input onChange={(e) => setInputField(e.target.value)} />
<button onClick={() => searchUser()}> Search </button>
{users.map((userData, i) => (
<Users
key={i}
firstName={userData.firstName}
lastName={userData.lastName}
email={userData.email}
bio={userData.bio}
/>
))}
</div>
);
}

useEffect keep fetching my data if I add state in useEffect

when I added eventList into my useEffect in an array like this:
const getEvents = async () => {
const res = await axios.get(`/api/v1/events/event/${teamId}`);
setEventList(res.data.Events);
};
useEffect(() => {
getEvents();
}, [eventList]);
It just keep fetching data non stop
but if I don't put eventList in there my page just fetch it one time and when I click to another component is will gone
Added Component:
Calendar.js:
const Calendar = (props) => {
const { teamId } = props;
const events = [];
const [date, setDate] = useState(new Date());
const [eventList, setEventList] = useState([]);
const [expanded, setExpanded] = useState("");
const getEvents = async () => {
const res = await axios.get(`/api/v1/events/event/${teamId}`);
setEventList(res.data.Events);
};
useEffect(() => {
getEvents();
}, []);
const handleChangeAccordion = (panel) => (event, newExpanded) => {
setExpanded(newExpanded ? panel : false);
};
const handleChangeCalendar = (value) => {
const currentDate = moment(value).format("YYYY-MM-DD");
setDate(currentDate);
const currentEvents = events.find((event) =>
moment(currentDate).isSame(event.date, "day")
);
setEventList(currentEvents ? currentEvents.events : []);
};
const [showCalendarCard, setShowCalendarCard] = useState(false);
const addEvent = () => {
setShowCalendarCard(true);
};
return (
<div className="calendar-tab">
<div className="event-view-container">
<div className="event-date">
<p className="event-date-monthday">{moment(date).format("D")}</p>
<p className="event-date-weekday">{moment(date).format("dddd")}</p>
</div>
<div className="event-list">
{eventList.map((event, index) => (
<div>
<Accordion
key={`event-${index}`}
square
expanded={expanded === `event${index + 1}`}
onChange={handleChangeAccordion(`event${index + 1}`)}
>
<AccordionSummary>
<div className="event-list-item-header">
<span className="timestart">
{moment(event.timestart, "HH:mm:ss").format("h:mm A")}
</span>
<span className="dash">-</span>
<span className="timeend">
{moment(event.timeend, "HH:mm:ss").format("h:mm A")}
</span>
<span className="title">{event.title}</span>
</div>
</AccordionSummary>
</Accordion>
<div className="event-list-item-content">
<div className="header">
<span className="announcements">Announcements</span>
<div className="plus">
<ControlPoint />
</div>
</div>
<div className="content">{event.description}</div>
</div>
</div>
))}
</div>
</div>
<div className="calendar-view-container">
<div className="event-calendar-container">
{!showCalendarCard ? (
<div>
<EventCalendar
className="event-calendar"
formatShortWeekday={(locale, date) =>
moment(date).format("dd").charAt(0)
}
tileClassName={({ date }) => {
if (events.find((x) => moment(x.date).isSame(date, "day"))) {
return "highlight";
}
}}
onChange={(value) => handleChangeCalendar(value)}
nextLabel={<NavigateNext />}
prevLabel={<NavigateBefore />}
/>
<div className="add-event">
<ControlPoint onClick={addEvent} />
</div>
</div>
) : (
<CalendarCard
setShowCalendarCard={setShowCalendarCard}
teamId={teamId}
/>
)}
</div>
</div>
</div>
);
};
export default Calendar;
When I click to another day in calendar it will disspear my list of events.
Here is the photo of my project:
Your useEffect is depending on eventList to change and by calling that function you are changing eventList, If you want to send the request only once then this solution will do
const Calendar = (props) => {
const { teamId } = props;
// const events = [];
const [date, setDate] = useState(new Date());
const [eventList, setEventList] = useState([]);
const [expanded, setExpanded] = useState("");
const getEvents = async () => {
const res = await axios.get(`/api/v1/events/event/${teamId}`);
setEventList(res.data.Events);
};
useEffect(() => {
getEvents();
}, []);
Having an empty dependant list will only trigger the function once so change
useEffect(() => {
getEvents();
}, [eventList]);
To this
useEffect(() => {
getEvents();
}, []);
Its stuck in a loop because your useEffect callback has the side-effect changing it's own dependency (eventList).
Basically instead of resetting the event list you fetched from the API, memoize the filtered list
e.g.
const filteredEventList = useMemo(() => {
return eventList.filter((event) =>
moment(date).isSame(event.date, "day")
)
}, [date, eventList])
and then use this filtered event list in your render

how can I handle variables reacts as variable - property?

I am trying to organize my code order to handle feed as feed.* based on my endpoint API, but however react doesn't allow me to directly send functions into component, but I want something similar to feed.results, feed. count
const [initialized, setIntialized] = useState(false);
const [feed, setFeed] = useState([]);
const browserFeed = async () => {
const response = await browse();
setFeed(response.results);
setIntialized(true);
};
useEffect(() => {
if (!initialized) {
browserFeed();
}
});
export const browse = () => {
return api.get('xxxxxxxx')
.then(function(response){
return response.data // returns .count , .next, .previous, and .results
})
.catch(function(error){
console.log(error);
});
}
<div className="searched-jobs">
<div className="searched-bar">
<div className="searched-show">Showing {feed.count}</div>
<div className="searched-sort">Sort by: <span className="post-time">Newest Post </span><span className="menu-icon">▼</span></div>
</div>
<div className="job-overview">
<div className="job-overview-cards">
<FeedsList feeds={feed} />
<div class="job-card-buttons">
<button class="search-buttons card-buttons-msg">Back</button>
<button class="search-buttons card-buttons">Next</button>
</div>
</div>
</div>
</div>
If it is pagination you are trying to handle here is one solution:
async function fetchFeed(page) {
return api.get(`https://example.com/feed?page=${page}`);
}
const MyComponent = () => {
const [currentPage, setCurrentPage] = useState(1);
const [feed, setFeed] = useState([]);
// Fetch on first render
useEffect(() => {
fetchFeed(1).then((data) => setFeed(data));
}, []);
// Update feed if the user changes the page
useEffect(() => {
fetchFeed(currentPage).then((data) => setFeed(data));
}, [currentPage]);
const isFirstPage = currentPage === 1;
return (
<>
<FeedsList feeds={feed} />
{isFirstPage && (
<button onClick={() => setCurrentPage(currentPage - 1)}>Back</button>
)}
<button Click={() => setCurrentPage(currentPage + 1)}>Next</button>
</>
);
};

How can I edit a todo in my react app using hooks?

I'm trying to figure out how to edit a todo item in my react app using hooks, but I can't seem to figure out how to write the code.
Most of the solutions I've seen online are using class components and it's not written with the same logic as my app.
Here is my current code
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = todo => {
if (!todo.text || /^\s*$/.test(todo.text)) {
return;
}
const newTodos = [todo, ...todos];
setTodos(newTodos);
console.log(newTodos);
};
const removeTodo = id => {
const removedArr = [...todos].filter(todoId => todoId.id !== id);
setTodos(removedArr);
};
const completeTodo = id => {
let updatedTodos = todos.map(todo => {
if (todo.id === id) {
todo.isComplete = !todo.isComplete;
}
return todo;
});
setTodos(updatedTodos);
};
const editTodo = e => {
setTodos(e.target.value);
};
return (
<>
<TodoForm onSubmit={addTodo} />
{todos.map(todo => (
<div>
<div
key={todo.id}
className={todo.isComplete ? 'complete' : ''}
key={todo.id}
onClick={() => completeTodo(todo.id)}
>
{todo.text}
</div>
<FaWindowClose onClick={() => removeTodo(todo.id)} />
</div>
))}
</>
);
}
Here is the code from the other component
function TodoForm(props) {
const [input, setInput] = useState('');
const handleChange = e => {
setInput(e.target.value);
};
const handleSubmit = e => {
e.preventDefault();
props.onSubmit({
id: Math.floor(Math.random() * 10000),
text: input,
complete: false
});
setInput('');
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder='todo...'
value={input}
onChange={handleChange}
name='text'
/>
<button onClick={handleSubmit}>add todo</button>
</form>
);
}
So right now everything works where I can add todos and delete todos + cross out todos. Only thing missing is being able to edit them.
I saw some suggestions about updating the text value with an input form, but I'm not too sure how I'd implement that in my editTodo function.
Similar to your removeTodo handler, you want to pass the todo.id to completeTodo.
<div className={todo.isComplete ? "complete" : ""} key={todo.id} onClick={() => completeTodo(todo.id)}>
Then you would update a bool value in the todo object.
const completeTodo = (id) => {
let updatedTodos = todos.map(todo => {
if(todo.id === id){
todo.isComplete = true
}
return todo
})
setTodos(updatedTodos)
};
Edit: add styling strikethrough
You'll then conditionally add a css style based on isComplete boolean
CSS
.complete {
text-decoration: line-through;
}
To be able to click on the Remove button, place it outside the todo div in your map function.
{todos.map((todo, isComplete) => (
<>
<div
key={todo.id}
onClick={completeTodo}
className={isComplete ? 'complete' : ''}
>
{todo.text}
</div>
<FaWindowClose onClick={() => removeTodo(todo.id)} />
</>
))}
As discussion with you in another question here it is:
TodoList.js
import React, { useState } from "react";
import TodoForm from "./TodoForm";
import Todo from "./Todo";
function TodoList({ onClick }) {
const [todos, setTodos] = useState([]);
//Track is edit clicked or not
const [editId, setEdit] = useState(false);
//Save input value in input box
const [inputValue, setInputValue] = useState("");
const handleEditChange = (id, text) => {
setEdit(id);
setInputValue(text);
};
const addTodo = (todo) => {
if (!todo.text || /^\s*$/.test(todo.text)) {
return;
}
const newTodos = [todo, ...todos];
setTodos(newTodos);
console.log(newTodos);
};
const removeTodo = (id) => {
const removedArr = [...todos].filter((todoId) => todoId.id !== id);
setTodos(removedArr);
};
const completeTodo = (id) => {
let updatedTodos = todos.map((todo) => {
if (todo.id === id) {
todo.isComplete = !todo.isComplete;
}
return todo;
});
setTodos(updatedTodos);
};
const editTodo = (id, text) => {
let editTodos = todos.map((todo) => {
if (todo.id === id) {
todo.text = text;
}
return todo;
});
setTodos(editTodos);
setEdit(false);
};
return (
<>
<TodoForm onSubmit={addTodo} />
{/* I want to move this code below into a new component called Todo.js */}
<Todo
todos={todos}
completeTodo={completeTodo}
removeTodo={removeTodo}
editTodo={editTodo}
handleEditChange={handleEditChange}
editId={editId}
inputValue={inputValue}
setInputValue={setInputValue}
/>
</>
);
}
export default TodoList;
Todo.js
// I want to move this code into this component
import React, { useState } from "react";
import { FaWindowClose, FaRegEdit } from "react-icons/fa";
const Todo = ({
todos,
completeTodo,
removeTodo,
editTodo,
editId,
handleEditChange,
inputValue,
setInputValue
}) => {
return todos.map((todo) => (
<div className="todo-row">
{editId === todo.id ? (
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
) : (
<div
key={todo.id}
className={todo.isComplete ? "complete" : ""}
onClick={() => completeTodo(todo.id)}
>
{todo.text}
</div>
)}
{editId === todo.id ? (
<button onClick={() => editTodo(todo.id, inputValue)}>Edit todo</button>
) : (
<>
<FaWindowClose onClick={() => removeTodo(todo.id)} />
<FaRegEdit onClick={() => handleEditChange(todo.id, todo.text)} />
</>
)}
</div>
));
};
export default Todo;
Make sure you read and understand code first. Logic is pretty simple what you do in completeTodo. You just need to update text part. Tricky part is to open in input. So logic is like track if user click on id set that id. And check if id is there open input with that id value other wise normal one.
Here is demo of this POC: https://codesandbox.io/s/nostalgic-silence-idm21?file=/src/Todo.js:0-1059

Resources