I'm reading the answer of this question: Problem with conditional statements in React Hook how can I Rewrite this code with multiple use state?
in this answer gather all the state objects to a single state.is there any way to rewrite this code with multiple and separate state?
const initialState = {
movies: [],
loading: true,
searchKeyword: "",
filterOverSeven: false,
filterByWatch: "ALL" // Can be "ALL" | "WATCHED" | "NOT_WATCHED"
};
const App = () => {
const [state, setState] = useState(initialState);
useEffect(() => {
fetch("https://my-json-server.typicode.com/bemaxima/fake-api/movies")
.then((response) => response.json())
.then((response) => {
setState((s) => ({
...s,
movies: response.map((item) => ({
id: item.id,
name: item.name,
rate: item.rate,
watched: false
})),
loading: false
}));
});
}, []);
function handleWatchedBtn(id) {
setState((s) => ({
...s,
movies: s.movies.map((movie) => {
if (movie.id === id) {
return { ...movie, watched: !movie.watched };
}
return movie;
})
}));
}
function handleKeywordChange(e) {
setState((s) => ({ ...s, searchKeyword: e.target.value }));
}
function handleOverSevenChange(e) {
setState((s) => ({ ...s, filterOverSeven: !s.filterOverSeven }));
}
function handleWatchedChange(filter) {
setState((s) => ({ ...s, filterByWatch: filter }));
}
function filterItems() {
return state.movies
.filter((item) =>
item.name.toLowerCase().includes(state.searchKeyword.toLowerCase())
)
.filter((item) => (state.filterOverSeven ? item.rate > 7 : true))
.filter((item) =>
state.filterByWatch === "ALL"
? true
: item.watched === (state.filterByWatch === "WATCHED")
);
}
if (state.loading) {
return "Please wait...";
}
return (
<div>
<div>
<div>
Keyword
<input
type="text"
value={state.searchKeyword}
onChange={handleKeywordChange}
/>
</div>
<div>
<button onClick={() => handleWatchedChange("ALL")}>all</button>
<button onClick={() => handleWatchedChange("WATCHED")}>watch</button>
<button onClick={() => handleWatchedChange("NOT_WATCHED")}>
not watch
</button>
</div>
<div>
Only over 7.0
<input
type="checkbox"
checked={state.filterOverSeven}
onChange={handleOverSevenChange}
/>
</div>
<div>
<ul>
{filterItems().map((movie) => (
<li data-id={movie.id}>
{`${movie.id} : ${movie.name} ${movie.rate}`}{" "}
<button onClick={() => handleWatchedBtn(movie.id)}>
{movie.watched ? "Watched" : " Not watched"}
</button>
</li>
))}
</ul>
</div>
</div>
</div>
);
};
export default App;
this is my code and how I try to change that.
import React, { useEffect, useState } from "react";
import { getAllMovies } from "./components/Transportlayer";
const App = () => {
const [movies, setMovies] = useState([]);
const [Keyword, setKeyword] = useState("");
const [OverSeven, setOverSeven] = useState(false);
const [Loading, setLoading] = useState(true);
const [filterByWatch, setfilterByWatch] = useState("ALL");
useEffect(() => {
getAllMovies().then((response) => {
setMovies(
response.map((item) => ({
id: item.id,
name: item.name,
rate: item.rate,
watched: false,
}))
);
setLoading(false);
});
}, []);
function handleWatchedBtn(id) {
setMovies({
movies: movies.map((movie) => {
if (movie.id === id) {
return { movie, watched: !movie.watched };
}
return movie;
})
});
}
function handleKeywordChange(e) {
setKeyword(e.target.value);
}
function handleOverSevenChange(e) {
setOverSeven(e.target.checked);
}
function handleWatchedChange(filter) {
setfilterByWatch({ filterByWatch: filter });
}
function filterItems() {
return movies
.filter((item) =>
item.name.toLowerCase().includes(Keyword.toLowerCase())
)
.filter((item) => (OverSeven ? item.rate > 7 : true))
.filter((item) =>
filterByWatch === "ALL"
? true
: item.watched === (filterByWatch === "WATCHED")
);
}
if (Loading) {
return "Please wait...";
}
return (
<div>
<div>
<div>
Keyword
<input
type="text"
value={Keyword}
onChange={handleKeywordChange}
/>
</div>
<div>
<button onClick={() => handleWatchedChange("ALL")}>all</button>
<button onClick={() => handleWatchedChange("WATCHED")}>done</button>
<button onClick={() => handleWatchedChange("NOT_WATCHED")}>
undone
</button>
</div>
<div>
Only over 7.0
<input
type="checkbox"
checked={OverSeven}
onChange={handleOverSevenChange}
/>
</div>
<div>
<ul>
{filterItems().map((movie) => (
<li data-id={movie.id}>
{`${movie.name} ${movie.rate}`}
<button onClick={() => handleWatchedBtn(movie.id)}>
{movie.watched ? "done" : " undone"}
</button>
</li>
))}
</ul>
</div>
</div>
</div>
);
};
export default App;
In the second code (separated states), you define movies as an array, but you are changing it to an object:
function handleWatchedBtn(id) {
setMovies({
movies: movies.map((movie) => {
if (movie.id === id) {
return { movie, watched: !movie.watched };
}
return movie;
})
});
}
I think this would work:
function handleWatchedBtn(id) {
setMovies(
movies.map((movie) => {
if (movie.id === id) {
return { movie, watched: !movie.watched };
}
return movie;
})
);
}
Same with the filterByWatch state, its defined to be a string but in here you are setting to it to a object:
function handleWatchedChange(filter) {
setfilterByWatch({ filterByWatch: filter });
}
correct:
function handleWatchedChange(value) {
setfilterByWatch(value);
}
This functions is wrong
function handleWatchedBtn(id) {
setMovies({
movies: movies.map((movie) => {
if (movie.id === id) {
return { movie, watched: !movie.watched };
}
return movie;
})
});
}
you are returning an object with key movies inside movies state, but movies should be an array of movies. Try something like this:
function handleWatchedBtn(id) {
setMovies(() => movies.map((movie) => {
if (movie.id === id) {
return { movie, watched: !movie.watched };
}
return movie;
)
});
}
This is the answer given by #Luca Pizzini and work for this problem.
change button text and state by click and show list in react hooks
You can use useReducer hook to better refactor this code.
Related
I'm making a todo list in react js. Each time a new todo item is created, some buttons are appended next to it along with a edit input text box. I'm trying to avoid using refs but purely usestate for my case, however I can't figure out how to do it. At its current state, all edit text inputs are using the same state and that brings focus loss along with other issues. I'd highly appreciate any suggetsions.
import "./theme.css"
import * as appStyles from "./styles/App.module.css"
import * as todoStyles from "./styles/Todo.module.css"
import { useState } from "react"
const initialState = [
{
id: "1",
name: "My first ToDo",
status: "new",
},
]
export function App() {
const [numofItems, setNumofItems] = useState(2)
const [newToDo, setnewToDo] = useState('');
const [todos, setTodos] = useState(initialState);
const [editTodo, setEditTodo] = useState({name: ""});
const onAddTodo = () => {
setnewToDo("");
setTodos((old) => [
...old,
{ id: numofItems.toString(), name: newToDo, status: "new" },
])
setNumofItems(numofItems + 1);
}
deleteList = () =>{
setTodos([]);
}
const handleEdit = (id, description) =>{
let el = todos.map((item) => {if(item.id === id) {item.name = description} return item});
setTodos(el);
setEditTodo('');
}
const handleMove = (id, position) =>{
const search = obj => obj.id === id;
const todoIndex = todos.findIndex(search);
if(position === "up"){
if (todos[todoIndex - 1] === undefined) {
} else {
const newTodo1 = [...todos];
const temp1 = newTodo1[todoIndex - 1];
const temp2 = newTodo1[todoIndex]
newTodo1.splice(todoIndex - 1, 1, temp2);
newTodo1.splice(todoIndex, 1, temp1);
setTodos([...newTodo1]);
}
}
else if(position === "down"){
if (todos[todoIndex + 1] === undefined) {
} else {
const newTodo1 = [...todos];
const temp1 = newTodo1[todoIndex + 1];
const temp2 = newTodo1[todoIndex]
newTodo1.splice(todoIndex + 1, 1, temp2);
newTodo1.splice(todoIndex, 1, temp1);
setTodos([...newTodo1]);
}
}
}
const Todo = ({ record }) => {
return <li className={todoStyles.item}>{record.name}
<button className={appStyles.editButtons} onClick={() => deleteListItem(record.id)} >Delete</button>
<button className={appStyles.editButtons} onClick={() => handleEdit(record.id, editTodo.name)}>Edit</button>
<button className={appStyles.editButtons} onClick={() => handleMove(record.id, "down")}>Move down</button>
<button className={appStyles.editButtons} onClick={() => handleMove(record.id, "up")}>Move up</button>
<input className={appStyles.input}
type = "text"
name={`editTodo_${record.id}`}
value = {editTodo.name}
onChange={event => {event.persist();
setEditTodo({name: event.target.value});}}
/></li>
}
const deleteListItem = (todoid) => {
setTodos(todos.filter(({id}) => id !== todoid))
}
return (
<>
<h3 className={appStyles.title}>React ToDo App</h3>
<ul className={appStyles.list}>
{todos.map((t, idx) => (
<Todo key={`todo_${idx}`} record={t} />
))}
</ul>
<div className={appStyles.actions}>
<form>
<label>
Enter new item:
<input className={appStyles.input} type="text" name="newToDo" value={newToDo} onChange={event => setnewToDo(event.target.value)}/>
</label>
</form>
<button
className={appStyles.button}
onClick={onAddTodo}
>
Add
</button>
<br></br>
<button className={appStyles.button} onClick={this.deleteList}>
Delete List
</button>
</div>
</>
)
}
Never define components in the body of another component. It will result in unmount/mount of that element every time it's rendered.
Here is how you can split up the Todo component from you App:
const Todo = ({ record, onDelete, onEdit, onMove }) => {
const [inputValue, setInputValue] = useState(record.name);
return (
<li className={todoStyles.item}>
{record.name}
<button className={appStyles.editButtons} onClick={() => onDelete()}>
Delete
</button>
<button
className={appStyles.editButtons}
onClick={() => onEdit(inputValue)}
>
Edit
</button>
<button className={appStyles.editButtons} onClick={() => onMove("down")}>
Move down
</button>
<button className={appStyles.editButtons} onClick={() => onMove("up")}>
Move up
</button>
<input
className={appStyles.input}
type="text"
value={inputValue}
onChange={(event) => {
setInputValue(event.target.value);
}}
/>
</li>
);
};
function App() {
return (
<>
<ul className={appStyles.list}>
{todos.map((t, idx) => (
<Todo
key={`todo_${idx}`}
record={t}
onDelete={() => deleteListItem(t.id)}
onEdit={(description) => handleEdit(t.id, description)}
onMove={(position) => handleMove(t.id, position)}
/>
))}
</ul>
</>
);
}
Note: I've shown only the interesting bits, not your entire code.
If you're going to do it this way I would suggest using useReducer instead of useState.
const initialState = [
{
id: "1",
name: "My first ToDo",
status: "new",
},
]
export const types = {
INIT: 'init',
NEW: 'new'
}
export default function (state, action) {
switch (action.type) {
case types.INIT:
return initialState;
case types.NEW:
return { ...state, { ...action.item } };
default:
return state;
}
}
Now in your component you can use it like this:
import {useReducer} from 'react';
import reducer, { initialState, types } from './wherever';
const [state, dispatch] = useReducer(reducer, initialState);
const handleSubmit = (event) => {
event.preventDefault();
dispatch({ type: types.NEW, item: event.target.value });
}
I am building a clone of the Google Keep app with react js. I added all the basic functionality (expand the create area, add a note, delete it) but I can't seem to manage the edit part. Currently I am able to edit the inputs and store the values in the state, but how can I replace the initial input values for the new values that I type on the input?
This is Note component
export default function Note(props) {
const [editNote, setEditNote] = useState(false);
const [currentNote, setCurrentNote] = useState({
id: props.id,
editTitle: props.title,
editContent: props.content,
});
const handleDelete = () => {
props.deleteNote(props.id);
};
const handleEdit = () => {
setEditNote(true);
setCurrentNote((prevValue) => ({ ...prevValue }));
};
const handleInputEdit = (event) => {
const { name, value } = event.target;
setCurrentNote((prevValue) => ({
...prevValue,
[name]: value,
}));
};
const updateNote = () => {
setCurrentNote((prevValue, id) => {
if (currentNote.id === id) {
props.title = currentNote.editTitle;
props.content = currentNote.editContent;
} else {
return { ...prevValue };
}
});
setEditNote(false);
};
return (
<div>
{editNote ? (
<div className='note'>
<input
type='text'
name='edittitle'
defaultValue={currentNote.editTitle}
onChange={handleInputEdit}
className='edit-input'
/>
<textarea
name='editcontent'
defaultValue={currentNote.editContent}
row='1'
onChange={handleInputEdit}
className='edit-input'
/>
<button onClick={() => setEditNote(false)}>Cancel</button>
<button onClick={updateNote}>Save</button>
</div>
) : (
<div className='note' onDoubleClick={handleEdit}>
<h1>{props.title}</h1>
<p>{props.content}</p>
<button onClick={handleDelete}>DELETE</button>
</div>
)}
</div>
);
}
And this is the Container component where I am renderind the CreateArea and mapping the notes I create. I tried to map the notes again with the new values but it wasn't working.
export default function Container() {
const [notes, setNotes] = useState([]);
const addNote = (newNote) => {
setNotes((prevNotes) => {
return [...prevNotes, newNote];
});
};
const deleteNote = (id) => {
setNotes((prevNotes) => {
return prevNotes.filter((note, index) => {
return index !== id;
});
});
};
// const handleUpdateNote = (id, updatedNote) => {
// const updatedItem = notes.map((note, index) => {
// return index === id ? updatedNote : note;
// });
// setNotes(updatedItem);
// };
return (
<div>
<CreateArea addNote={addNote} />
{notes.map((note, index) => {
return (
<Note
key={index}
id={index}
title={note.title}
content={note.content}
deleteNote={deleteNote}
//handleUpdateNote={handleUpdateNote}
/>
);
})}
</div>
);
}
There are a couple of mistakes in your code.
The state properties are in the camel case
const [currentNote, setCurrentNote] = useState({
...
editTitle: props.title,
editContent: props.content,
});
But the names of the input are in lowercase.
<input
name='edittitle'
...
/>
<textarea
name='editcontent'
...
/>
Thus in handleInputEdit you don't update the state but add new properties: edittitle and editcontent. Change the names to the camel case.
In React you cant assign to the component prop values, they are read-only.
const updateNote = () => {
...
props.title = currentNote.editTitle;
props.content = currentNote.editContent;
You need to use the handleUpdateNote function passed by the parent component instead. You have it commented for some reason.
<Note
...
//handleUpdateNote={handleUpdateNote}
/>
Check the code below. I think it does what you need.
function Note({ id, title, content, handleUpdateNote, deleteNote }) {
const [editNote, setEditNote] = React.useState(false);
const [currentNote, setCurrentNote] = React.useState({
id,
editTitle: title,
editContent: content,
});
const handleDelete = () => {
deleteNote(id);
};
const handleEdit = () => {
setEditNote(true);
setCurrentNote((prevValue) => ({ ...prevValue }));
};
const handleInputEdit = (event) => {
const { name, value } = event.target;
setCurrentNote((prevValue) => ({
...prevValue,
[name]: value,
}));
};
const updateNote = () => {
handleUpdateNote({
id: currentNote.id,
title: currentNote.editTitle,
content: currentNote.editContent
});
setEditNote(false);
};
return (
<div>
{editNote ? (
<div className='note'>
<input
type='text'
name='editTitle'
defaultValue={currentNote.editTitle}
onChange={handleInputEdit}
className='edit-input'
/>
<textarea
name='editContent'
defaultValue={currentNote.editContent}
row='1'
onChange={handleInputEdit}
className='edit-input'
/>
<button onClick={() => setEditNote(false)}>Cancel</button>
<button onClick={updateNote}>Save</button>
</div>
) : (
<div className='note' onDoubleClick={handleEdit}>
<h1>{title}</h1>
<p>{content}</p>
<button onClick={handleDelete}>DELETE</button>
</div>
)}
</div>
);
}
function CreateArea() {
return null;
}
function Container() {
const [notes, setNotes] = React.useState([
{ title: 'Words', content: 'hello, bye' },
{ title: 'Food', content: 'milk, cheese' }
]);
const addNote = (newNote) => {
setNotes((prevNotes) => {
return [...prevNotes, newNote];
});
};
const deleteNote = (id) => {
setNotes((prevNotes) => {
return prevNotes.filter((note, index) => {
return index !== id;
});
});
};
const handleUpdateNote = ({ id, title, content }) => {
const _notes = [];
for (let i = 0; i < notes.length; i++) {
if (i === id) {
_notes.push({ id, title, content });
} else {
_notes.push(notes[i]);
}
}
setNotes(_notes);
};
return (
<div>
<CreateArea addNote={addNote} />
{notes.map((note, index) => {
return (
<Note
key={index}
id={index}
title={note.title}
content={note.content}
deleteNote={deleteNote}
handleUpdateNote={handleUpdateNote}
/>
);
})}
</div>
);
}
function App() {
return (
<div>
<Container />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
Also, you can store the notes in an object or hash map instead of an array. For example
const [notes, setNotes] = React.useState({
'unique_id': { title: 'Words', content: 'hello, bye' }
});
Then in handleUpdateNote you have
setNotes((prev) => ({ ...prev, unique_id: { title, content } }))
I'm new to Redux and React and I'm struggling with an issue for a while now. I have a reducer which contains a favorites array and another array storing the id's of the favorite movies.
Everything seemed to be working at first, but
the problem is when I open the app in another browser I receive error of *cannot read property filter / find of undefined, I get an error everywhere where I try to filter arrays of my reducer. What should I do differently ? I need to filter them because I want my buttons text to be 'Added' for the movies already in the watchlist. Is there something wrong with my reducer or is there another method I could use for filtering?
const intitialState = {
favorites: [],
favIds: [],
btnId: [],
isFavorite: false,
toggle: false,
};
const watchlistReducer = (state = intitialState, action) => {
console.log(action, state);
switch (action.type) {
case 'ADD_WATCHLIST':
return {
...state,
favorites: [...state.favorites, action.payload],
favIds: [
...state.favorites.map((movie) => movie.id),
action.payload.id,
],
btnId: action.payload.id,
isFavorite: true,
toggle: true,
};
case 'REMOVE_WATCHLIST': {
return {
...state,
favorites: [
...state.favorites.filter((movie) => movie.id !== action.payload.id),
],
favIds: [...state.favorites.map((movie) => movie.id)],
btnId: action.payload.id,
isFavorite: false,
toggle: false,
};
}
case 'CLEAR_ALL': {
return intitialState;
}
default:
return state;
}
};
export default watchlistReducer;
const MovieDetail = () => {
const dispatch = useDispatch();
const { movie, isLoading } = useSelector((state) => state.detail);
const { favorites, favIds } = useSelector((state) => state.watchlist);
const [addFav, setAddFav] = useState([favorites]);
//Toggle
const [btnText, setBtnText] = useState('Add to Watchlist');
const [isToggled, setIsToggled] = useLocalStorage('toggled', false);
const [selected, setSelected] = useState([favIds]);
const history = useHistory();
const exitDetailHandler = (e) => {
const element = e.target;
if (element.classList.contains('shadow')) {
document.body.style.overflow = 'auto';
history.push('/');
}
};
const shorten = (text, max) => {
return text && text.length > max
? text.slice(0, max).split(' ').slice(0, -1).join(' ')
: text;
};
const toggleClass = () => {
setIsToggled(!isToggled);
};
const changeText = (id) => {
const check = favIds.filter((id) => id === movie.id);
check.includes(id) ? setBtnText('Added') : setBtnText('Add to Watchlist');
};
const addFavHandler = (movie) => {
const checkMovie = favorites.find((fav) => fav.id === movie.id);
if (!checkMovie) {
dispatch(addWatchlist(movie));
setAddFav([...addFav, movie]);
setBtnText('Added');
} else if (checkMovie) {
alert('Remove from Watchlist?');
dispatch(removeWatchlist(movie));
setBtnText('Add to Watchlist');
} else {
setAddFav([...addFav]);
}
};
return (
<>
{!isLoading && (
<StyledCard>
<StyledDetails
onChange={() => changeText(movie.id)}
className='shadow'
style={{
backgroundImage: ` url("${bgPath}${movie.backdrop_path}")`,
backGroundPosition: 'center center',
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundBlendMode: 'multiply',
}}
onClick={exitDetailHandler}
>
<StyledInfo>
<StyledMedia>
<img
src={
movie.poster_path || movie.backdrop_path
? `${imgPath}${movie.poster_path}`
: unavailable
}
alt='movie'
/>
</StyledMedia>
<StyledDescription>
<div className='genre'>
<h3>{movie.title}</h3>
<p>
{movie.genres &&
movie.genres.map((genre) => (
<span key={genre.id}>{genre.name}</span>
))}
</p>
</div>
<div className='stats'>
<p>
Release Date: <br />{' '}
<span>
{' '}
{movie.release_date ? movie.release_date : ` N / A`}{' '}
</span>
</p>
<p>
Rating: <br /> <span> {movie.vote_average} </span>
</p>
<p>
Runtime: <br /> <span>{movie.runtime} min</span>
</p>
</div>
<div className='synopsys'>
<p>
{shorten(`${movie.overview}`, 260)}
...
</p>
</div>
<button
className={btnText === 'Added' ? 'added' : 'btn'}
id={movie.id}
key={movie.id}
value={movie.id}
type='submit'
onClick={() => {
toggleClass();
addFavHandler(movie);
}}
>
{btnText}
</button>
</StyledDescription>
</StyledInfo>
</StyledDetails>
</StyledCard>
)}
</>
);
};
I'm new to the react and I would like to know I made a function that makes filtering depending on the boolean values
let [filterState, setFilterSTate] = useState("all");
let filteredUser = todos.filter((е) => {
if (filterState === "active") {
return !е.done;
}
if (filterState === "completed") {
return е.done;
}
return true;
});
in the console, everything is displayed, but I can't figure out how to make it work in the browser itself and only the necessary tricks are displayed
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./index.css";
const TodoList = () => {
const [newTodo, setNewTodo] = useState("");
const [todos, setTodos] = useState([
{ done: true, text: "Hey", id: 1 },
{ done: false, text: "There", id: 2 },
{ done: false, text: "Dima", id: 3 },
]);
const [id, setId] = useState(4);
const toggleDone = (id) => {
setTodos(
todos.map((todo) => ({
...todo,
done: id === todo.id ? !todo.done : todo.done,
}))
);
};
const updateTodo = (id, e) => {
setTodos(
todos.map((todo) => ({
...todo,
text: id === todo.id ? e.target.value : todo.text,
}))
);
};
const onDelete = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
const updateNewTodo = (e) => {
setNewTodo(e.target.value);
};
const onAdd = () => {
setTodos([
...todos,
{
text: newTodo,
done: false,
id,
},
]);
setId(id + 1);
setNewTodo("");
};
let [filterState, setFilterSTate] = useState("all");
let filteredUser = todos.filter((е) => {
if (filterState === "active") {
return !е.done;
}
if (filterState === "completed") {
return е.done;
}
return true;
});
// let filteredComplited = todos.filter(function (e) {
// return e.done === true;
// });
// console.log("filteredComplited", filteredComplited);
// let filteredActive = todos.filter(function (e) {
// return e.done === false;
// });
// console.log("filteredActive", filteredActive);
// let filteredAll = todos;
// console.log("filteredAll", filteredAll);
return (
<div className="todos">
Todo List
{todos.map((todo) => (
<div key={todo.id} className="todo">
<input
type="checkbox"
value={todo.done}
onChange={() => toggleDone(todo.id)}
/>
<input
type="text"
value={todo.text}
onChange={(evt) => updateTodo(todo.id, evt)}
/>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
))}
<div className="todo">
<input type="text" value={newTodo} onChange={updateNewTodo} />
<button onClick={() => onAdd()}>Add</button>
</div>
<button
onclick={() =>
todos.map((t) => `${t.done ? "x" : ""} ${t.text}`).join("\n")
}
>
Save
</button>
<button onClick={() => setFilterSTate("completed")}>Complited</button>
<button onClick={() => setFilterSTate("active")}>Active</button>
<button onClick={() => setFilterSTate("all")}>All</button>
<pre>filterState: {JSON.stringify(filterState, null, 2)}</pre>
<br />
<pre>{JSON.stringify(filteredUser, null, 2)}</pre>
</div>
);
};
ReactDOM.render(<TodoList />, document.getElementById("todos"));
I am facing an Issue on the navigation function of a form I am building.
The below and linked code is the dummiest version possible to try an shorten and simplify the issue.
I will post my whole code below and you can run it here as well.
It's a form that the user fills and navigates from step to step until the final submission of the data.
The navigation buttons work well, using this.inputMenuNavigationHandler(App.js Line: 25), going from section to section, but on a specific section I have created kind of sub-sections, which are alternated using this.subOptionNavigation function.
When I alternate the info inside case "foo2"(switch function of Input.js - line: 15) and afterwards call this.inputMenuNavigationHandler again, for some reason the sections get out of order, and instead of navigating from secondMenu to thirdMenu, it takes me to firstMenu.
I do not understand why this is happening, as the navigation function, which uses the classArray.indexOf(classElement), in other words, the order of the array of objects, does not interfere or relates to what the value of this.state.newClassForm.secondMenu.internalNavigation is...
App.js File
import React from "react";
import Input from "./Input";
import "./styles.css";
export default class App extends React.Component {
state = {
classNavigation: "firstMenu",
newClassForm: {
firstMenu: {
elementType: "foo1",
title: "First Section"
},
secondMenu: {
elementType: "foo2",
title: "Second Section",
internalNavigation: "tba"
},
thirdMenu: {
elementType: "foo3",
title: "Third Section"
}
}
};
inputMenuNavigationHandler = (classArray, classElement, command) => {
let index = classArray.indexOf(classElement);
if (command === "increment" && index < classArray.length - 1) {
let incrementModal = classArray[index + 1].id;
this.setState(({ classNavigation, ...restTop }) => ({
classNavigation: incrementModal,
...restTop
}));
}
if (command === "decrement" && index > 0) {
let decrementModal = classArray[index - 1].id;
this.setState(({ classNavigation, ...restTop }) => ({
classNavigation: decrementModal,
...restTop
}));
}
};
subOptionNavigation = childSubOption => {
this.setState(
({
newClassForm: {
secondMenu: { internalNavigation, ...restSecondMenu },
...restNewClassForm
},
...restTop
}) => ({
newClassForm: {
secondMenu: { internalNavigation: childSubOption, ...restSecondMenu },
...restNewClassForm
},
...restTop
})
);
};
render() {
let classArray = [];
for (let key in { ...this.state.newClassForm }) {
classArray.push({
id: key,
config: this.state.newClassForm[key]
});
}
return (
<div className="App">
{classArray.map(cl =>
cl.id.indexOf(this.state.classNavigation) !== -1 ? (
<div
key={cl.id}
style={{
padding: "10px",
border: "1px solid black",
margin: "5px"
}}
>
<Input
classArray={classArray}
classElement={cl}
elementType={cl.config.elementType}
title={cl.config.title}
navigationIncrement={() =>
this.inputMenuNavigationHandler(classArray, cl, "increment")
}
navigationDecrement={() =>
this.inputMenuNavigationHandler(classArray, cl, "decrement")
}
internalNavigationDisplay={
this.state.newClassForm.secondMenu.internalNavigation
}
toOption1={() => this.subOptionNavigation("subOption1")}
toOption2={() => this.subOptionNavigation("subOption2")}
/>
</div>
) : null
)}
</div>
);
}
}
Input.js
import React from "react";
export default function Input(props) {
let input = null;
switch (props.elementType) {
case "foo1":
input = (
<div>
<div>{props.title}</div>
<button onClick={props.navigationDecrement}>Previous</button>
<button onClick={props.navigationIncrement}>Next</button>
</div>
);
break;
case "foo2":
input = (
<div>
{props.internalNavigationDisplay === "tba" ? (
<div>
<div>{props.title}</div>
<button onClick={props.toOption1}>Option 1</button>
<button onClick={props.toOption2}>Option 2</button>
</div>
) : null}
{props.internalNavigationDisplay === "subOption1" ? (
<div>
<div>SubOption 1</div>
<button onClick={props.navigationDecrement}>Previous</button>
<button onClick={props.navigationIncrement}>Next</button>
</div>
) : null}
{props.internalNavigationDisplay === "subOption2" ? (
<div>
<div>SubOption 2</div>
<button onClick={props.navigationDecrement}>Previous</button>
<button onClick={props.navigationIncrement}>Next</button>
</div>
) : null}
</div>
);
break;
case "foo3":
input = (
<div>
<div>{props.title}</div>
<button onClick={props.navigationDecrement}>Previous</button>
<button onClick={props.navigationIncrement}>Next</button>
</div>
);
break;
default:
input = null;
}
return { ...input };
}
Well!!!
Here is my solution in order to preserve the index
I have created a button, which will start the navigation:
<div className="App">
<h1>Create a new class?</h1>
<button onClick={this.newClassHandler}>Go!!</button>
{this.state.createNewClass
? this.state.newClassArray.map //...map the stuff and render the input sections
So on the newClassHandler function I move the code that once was in my render part of the component, which was the reason why the index was getting screwed up, as some bug may happen when I navigate into the sub section of the second input, once the component re renders.
Does any one have a solution which does not require the use of state???
import React from "react";
import Input from "./Input";
import "./styles.css";
export default class App extends React.Component {
state = {
indexOfNavigation: 0,
createNewClass: false,
newClassArray: [],
classNavigation: "firstMenu",
newClassForm: {
firstMenu: {
elementType: "foo1",
title: "First Section"
},
secondMenu: {
elementType: "foo2",
title: "Second Section",
internalNavigation: "tba"
},
thirdMenu: {
elementType: "foo3",
title: "Third Section"
}
}
};
newClassHandler = () => {
let classArray = [];
for (let key in { ...this.state.newClassForm }) {
classArray.push({
id: key,
config: this.state.newClassForm[key]
});
}
this.setState(({ createNewClass, newClassArray, ...restTop }) => ({
createNewClass: true,
newClassArray: classArray,
...restTop
}));
};
inputMenuNavigationHandler = (classArray, classElement, command) => {
let index = classArray.indexOf(classElement);
console.log("index before setting my state", index);
console.log("element Id before navigating - ", classElement.id);
this.setState(
({ indexOfNavigation, ...restTop }) => ({
indexOfNavigation: index,
...restTop
}),
() => {
if (
command === "increment" &&
this.state.indexOfNavigation < classArray.length - 1
) {
this.setState(
({ indexOfNavigation, ...restTop }) => ({
indexOfNavigation: indexOfNavigation + 1,
...restTop
}),
() => {
console.log(
"index after setting state",
this.state.indexOfNavigation
);
let incrementModal = classArray[this.state.indexOfNavigation].id;
this.setState(
({ classNavigation, ...restTop }) => ({
classNavigation: incrementModal,
...restTop
}),
() => {
console.log(
"element Id being received after navigating",
classElement.id
);
console.log(
"classNavigation info after navigating",
this.state.classNavigation
);
}
);
}
);
}
if (command === "decrement" && this.state.indexOfNavigation > 0) {
this.setState(
({ indexOfNavigation, ...restTop }) => ({
indexOfNavigation: indexOfNavigation - 1,
...restTop
}),
() => {
console.log(
"index after setting state",
this.state.indexOfNavigation
);
let incrementModal = classArray[this.state.indexOfNavigation].id;
this.setState(
({ classNavigation, ...restTop }) => ({
classNavigation: incrementModal,
...restTop
}),
() => {
console.log(
"element Id being received after navigating",
classElement.id
);
console.log(
"classNavigation info after navigating",
this.state.classNavigation
);
}
);
}
);
}
}
);
};
subOptionNavigation = childSubOption => {
this.setState(
({
newClassForm: {
secondMenu: { internalNavigation, ...restSecondMenu },
...restNewClassForm
},
...restTop
}) => ({
newClassForm: {
secondMenu: { internalNavigation: childSubOption, ...restSecondMenu },
...restNewClassForm
},
...restTop
}),
() => {
console.log(
"index of main array once navigating in the sub menu",
this.state.indexOfNavigation
);
console.log(
"classNavigation info after navigating in sub menus",
this.state.classNavigation
);
}
);
};
render() {
//here I change the format of the new class form object
let classArray = [];
for (let key in { ...this.state.newClassForm }) {
classArray.push({
id: key,
config: this.state.newClassForm[key]
});
}
return (
<div className="App">
<h1>Create a new class?</h1>
<button onClick={this.newClassHandler}>Go!!</button>
{this.state.createNewClass
? this.state.newClassArray.map(cl =>
cl.id.indexOf(this.state.classNavigation) !== -1 ? (
<div
key={cl.id}
style={{
padding: "10px",
border: "1px solid black",
margin: "5px"
}}
>
<Input
classArray={this.state.newClassArray}
classElement={cl}
elementType={cl.config.elementType}
title={cl.config.title}
navigationIncrement={() =>
this.inputMenuNavigationHandler(
this.state.newClassArray,
cl,
"increment"
)
}
navigationDecrement={() =>
this.inputMenuNavigationHandler(
this.state.newClassArray,
cl,
"decrement"
)
}
internalNavigationDisplay={
this.state.newClassForm.secondMenu.internalNavigation
}
toOption1={() => this.subOptionNavigation("subOption1")}
toOption2={() => this.subOptionNavigation("subOption2")}
/>
</div>
) : null
)
: null}
</div>
);
}
}
I could also preserve my array of manipulated objects in order to keep the array on the same order just by using a lifecycle hook:
moving the code from the render part, and storing in my state, and then rendering this state on input in order to run my logic, it works as well.
componentDidMount() {
console.log("component did mount");
let classArray = [];
for (let key in { ...this.state.newClassForm }) {
classArray.push({
id: key,
config: this.state.newClassForm[key]
});
}
this.setState(
({ newClassArray, ...restTop }) => ({
newClassArray: classArray,
...restTop
}),
() => console.log(this.state.newClassArray)
);
}