I am very new to react and node, I have managed to create an API for a simple todo list. I have fetched the data from the api and presenting it on the screen.
If I leave the dependency array empty on the useEffect() hook it will only render once and doesn't loop. But If I add a new Todo it will not update the list unless I refresh. So I put the todos state into the dependency array, this will then show the new item when I add it but if I look at the network tab in the dev tools its hitting the api in an infinite loop. What am I doing wrong ?
here is the code:
App
import React, { useState, useEffect } from "react";
import Todo from "./components/Todo";
import Heading from "./components/Heading";
import NewTodoForm from "./components/NewTodoForm";
const App = () => {
const [todos, setTodos] = useState([]);
useEffect(() => {
const getTodos = async () => {
const res = await fetch("http://localhost:3001/api/todos");
const data = await res.json();
setTodos(data);
};
getTodos();
}, []);
return (
<div className="container">
<Heading todos={todos} />
<section className="todos-container">
<ul className="todos">
{todos.map((todo) => (
<Todo key={todo._id} todo={todo} />
))}
</ul>
</section>
<section className="todo-form">
<NewTodoForm />
</section>
</div>
);
};
export default App;
Heading
import React from "react";
const Heading = ({ todos }) => (
<header>
<h1>Todos</h1>
<p>
{todos.length} {todos.length === 1 ? "Item" : "Items"}
</p>
</header>
);
export default Heading;
Todo
import React, { useState } from "react";
const Todo = ({ todo }) => (
<li>
{todo.name}
<input type="checkbox" />
</li>
);
export default Todo;
NewTodoForm
import React, { useState } from "react";
import { Plus } from "react-feather";
const NewTodoForm = () => {
const [formData, setFormData] = useState({
name: "",
completed: false,
});
const { name } = formData;
const handleOnChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = async (e) => {
e.preventDefault();
await fetch("http://localhost:3001/api/todos", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
setFormData({
name: "",
completed: false,
});
};
return (
<form onSubmit={handleSubmit}>
<div className="form-control">
<Plus className="plus" />
<input
name="name"
type="text"
placeholder="Add New Item"
onChange={handleOnChange}
value={name}
/>
<button>Add</button>
</div>
</form>
);
};
export default NewTodoForm;
If I comment all the components out and only have the App component it still infinite loops when I add todos to the dependency array of the useEffect() hook.
So instead of giving that as a dependency write the function outside the useEffect so that you can call that function after you add a todo
Example:
const getTodos = async () => {
const res = await fetch("http://localhost:3001/api/todos");
const data = await res.json();
setTodos(data);
};
useEffect(() => {
getTodos();
}, []);
So getTodos will only run once initially and runs again only on the onSubmit or onClick of your Todo, So, just call getTodos function onSubmit or onClick
Related
I'm having some issues getting a component to re-render after submitting a form. I created separate files to store these custom hooks to make them as reusable as possible. Everything is functioning correctly, except I haven't figured out a way to re render a list component after posting a new submit to that list. I am using axios for fetch requests and react-final-form for my actual form. Am I not able to re-render the component because I am using useContext to "share" my data across child components? My comments are set up as nested attributes to each post, which is being handled through Rails. My comment list is rendered in it's own component, where I call on the usePost() function in the PostContext.js file. I can provide more info/context if needed.
**
Also, on a slightly different note. I am having difficulty clearing the form inputs after a successful submit. I'm using react-final-form and most the suggestions I've seen online are for class components. Is there a solution for functional components?
react/contexts/PostContext.js
import React, { useContext, useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { useAsync } from "../hooks/useAsync";
import { getPost } from "../services/post";
const Context = React.createContext();
export const usePost = () => {
return useContext(Context);
};
export const PostProvider = ({ children }) => {
const id = useParams();
const { loading, error, value: post } = useAsync(() => getPost(id.id), [
id.id,
]);
const [comments, setComments] = useState([]);
useEffect(() => {
if (post?.comments == null) return;
setComments(post.comments);
}, [post?.comments]);
return (
<Context.Provider
value={{
post: { id, ...post },
comments: comments,
}}
>
{loading ? <h1>Loading</h1> : error ? <h1>{error}</h1> : children}
</Context.Provider>
);
};
react/services/comment.js
import { makeRequest } from "./makeRequest";
export const createComment = ({ message, postId }) => {
message["post_id"] = postId;
return makeRequest("/comments", {
method: "POST",
data: message,
}).then((res) => {
if (res.error) return alert(res.error);
});
};
react/services/makeRequest.js
import axios from "axios";
const api = axios.create({
baseURL: "/api/v1",
withCredentials: true,
});
export const makeRequest = (url, options) => {
return api(url, options)
.then((res) => res.data)
.catch((err) => Promise.reject(err?.response?.data?.message ?? "Error"));
};
react/components/Comment/CommentForm.js
import React from "react";
import { Form, Field } from "react-final-form";
import { usePost } from "../../contexts/PostContext";
import { createComment } from "../../services/comment";
import { useAsyncFn } from "../../hooks/useAsync";
const CommentForm = () => {
const { post, createLocalComment } = usePost();
const { loading, error, execute: createCommentFn } = useAsyncFn(
createComment
);
const onCommentCreate = (message) => {
return createCommentFn({ message, postId: post.id });
};
const handleSubmit = (values) => {
onCommentCreate(values);
};
return (
<Form onSubmit={handleSubmit}>
{({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<div className="comment-form-row">
<Field name="body">
{({ input }) => (
<textarea
className="comment-input"
placeholder="Your comment..."
type="text"
{...input}
/>
)}
</Field>
<button className="comment-submit-btn" type="submit">
Submit
</button>
</div>
</form>
)}
</Form>
);
};
export default CommentForm;
REACT.js:
Let say I have a home page with a search bar, and the search bar is a separate component file i'm calling.
The search bar file contains the useState, set to whatever the user selects. How do I pull that state from the search bar and give it to the original home page that
SearchBar is called in?
The SearchBar Code might look something like this..
import React, { useEffect, useState } from 'react'
import {DropdownButton, Dropdown} from 'react-bootstrap';
import axios from 'axios';
const StateSearch = () =>{
const [states, setStates] = useState([])
const [ stateChoice, setStateChoice] = useState("")
useEffect (()=>{
getStates();
},[])
const getStates = async () => {
let response = await axios.get('/states')
setStates(response.data)
}
const populateDropdown = () => {
return states.map((s)=>{
return (
<Dropdown.Item as="button" value={s.name}>{s.name}</Dropdown.Item>
)
})
}
const handleSubmit = (value) => {
setStateChoice(value);
}
return (
<div>
<DropdownButton
onClick={(e) => handleSubmit(e.target.value)}
id="state-dropdown-menu"
title="States"
>
{populateDropdown()}
</DropdownButton>
</div>
)
}
export default StateSearch;
and the home page looks like this
import React, { useContext, useState } from 'react'
import RenderJson from '../components/RenderJson';
import StateSearch from '../components/StateSearch';
import { AuthContext } from '../providers/AuthProvider';
const Home = () => {
const [stateChoice, setStateChoice] = useState('')
const auth = useContext(AuthContext)
console.log(stateChoice)
return(
<div>
<h1>Welcome!</h1>
<h2> Hey there! Glad to see you. Please login to save a route to your prefered locations, or use the finder below to search for your State</h2>
<StateSearch stateChoice={stateChoice} />
</div>
)
};
export default Home;
As you can see, these are two separate files, how do i send the selection the user makes on the search bar as props to the original home page? (or send the state, either one)
You just need to pass one callback into your child.
Homepage
<StateSearch stateChoice={stateChoice} sendSearchResult={value => {
// Your Selected value
}} />
Search bar
const StateSearch = ({ sendSearchResult }) => {
..... // Remaining Code
const handleSubmit = (value) => {
setStateChoice(value);
sendSearchResult(value);
}
You can lift the state up with function you pass via props.
const Home = () => {
const getChoice = (choice) => {
console.log(choice);
}
return <StateSearch stateChoice={stateChoice} giveChoice={getChoice} />
}
const StateSearch = (props) => {
const handleSubmit = (value) => {
props.giveChoice(value);
}
// Remaining code ...
}
Actually there is no need to have stateChoice state in StateSearch component if you are just sending the value up.
Hello and welcome to StackOverflow. I'd recommend using the below structure for an autocomplete search bar. There should be a stateless autocomplete UI component. It should be wrapped into a container that handles the search logic. And finally, pass the value to its parent when the user selects one.
// import { useState, useEffect } from 'react' --> with babel import
const { useState, useEffect } = React // --> with inline script tag
// Autocomplete.jsx
const Autocomplete = ({ onSearch, searchValue, onSelect, suggestionList }) => {
return (
<div>
<input
placeholder="Search!"
value={searchValue}
onChange={({target: { value }}) => onSearch(value)}
/>
<select
value="DEFAULT"
disabled={!suggestionList.length}
onChange={({target: {value}}) => onSelect(value)}
>
<option value="DEFAULT" disabled>Select!</option>
{suggestionList.map(({ id, value }) => (
<option key={id} value={value}>{value}</option>
))}
</select>
</div>
)
}
// SearchBarContainer.jsx
const SearchBarContainer = ({ onSelect }) => {
const [searchValue, setSearchValue] = useState('')
const [suggestionList, setSuggestionList] = useState([])
useEffect(() => {
if (searchValue) {
// some async logic that fetches suggestions based on the search value
setSuggestionList([
{ id: 1, value: `${searchValue} foo` },
{ id: 2, value: `${searchValue} bar` },
])
}
}, [searchValue, setSuggestionList])
return (
<Autocomplete
onSearch={setSearchValue}
searchValue={searchValue}
onSelect={onSelect}
suggestionList={suggestionList}
/>
)
}
// Home.jsx
const Home = ({ children }) => {
const [result, setResult] = useState('')
return (
<div>
<SearchBarContainer onSelect={setResult} />
result: {result}
</div>
)
}
ReactDOM.render(<Home />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Just pass a setState to component
parent component:
const [state, setState] = useState({
selectedItem: ''
})
<StateSearch state={state} setState={setState} />
change parent state from child component:
const StateSearch = ({ state, setState }) => {
const handleStateChange = (args) => setState({…state, selectedItem:args})
return (...
<button onClick={() => handleStateChange("myItem")}/>
...)
}
I've got a bug with LocalStorage on react.js. I try to set a todo into it, but it doesn't load. This is the code:
import React, { useState, useRef, useEffect } from 'react';
import './App.css';
import TodoList from './TodoList';
const { v4: uuidv4 } = require('uuid');
const LOCAL_STORAGE_KEY = 'todoApp.todos'
function App() {
const [todos, setTodos] = useState([]);
const TodoNameRef = useRef()
useEffect(() => {
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if (storedTodos) setTodos(storedTodos)
}, [])
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}, [todos])
function HandleAddTodo(e){
const name = TodoNameRef.current.value
if (name==='') return
setTodos(prevTodos => {
return[...prevTodos, { id:uuidv4(), name:name, complete:false}]
})
TodoNameRef.current.value = null
}
return (
<>
<TodoList todos={todos}/>
<input ref={TodoNameRef} type="text" />
<button onClick={HandleAddTodo}>Add todo</button>
<button>clear todo</button>
<p>0 left todo</p>
</>
)
}
export default App;
This is TodoList.js
import React from 'react'
import Todo from './Todo';
export default function TodoList({ todos }) {
return (
todos.map(todo =>{
return <Todo key ={todo.id} todo={todo} />
})
)
}
And as last Todo.js:
import React from 'react'
export default function Todo({ todo }) {
return (
<div>
<label>
<input type="checkbox" checked={todo.complete}/>
{todo.name}
</label>
</div>
)
}
What the code has to do is load a todo into the local storage, and after refreshing the page reload it into the document. The code I implemented
I just started with react but I hope anyone can pass me the right code to make it work. If anyone need extra explenation, say it to me.
Kind regards, anonymous
Try to decouple your local storage logic into it's own react hook. That way you can handle getting and setting the state and updating the local storage along the way, and more importantly, reuse it over multiple components.
The example below is way to implement this with a custom hook.
const useLocalStorage = (storageKey, defaultValue = null) => {
const [storage, setStorage] = useState(() => {
const storedData = localStorage.getItem(storageKey);
if (storedData === null) {
return defaultValue;
}
try {
const parsedStoredData = JSON.parse(storedData);
return parsedStoredData;
} catch(error) {
console.error(error);
return defaultValue;
}
});
useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify(storage));
}, [storage]);
return [storage, setStorage];
};
export default useLocalStorage;
And you'll use it just like how you would use a useState hook. (Under the surface it is not really more than a state with some side effects.)
const LOCAL_STORAGE_KEY = 'todoApp.todos'
function App() {
const [todos, setTodos] = useLocalStorage(LOCAL_STORAGE_KEY, []);
const handleAddTodo = event => {
setTodos(prevTodos => {
return[...prevTodos, {
id: uuidv4(),
name,
complete: false
}]
})
};
return (
<button onClick={HandleAddTodo}>Add todo</button>
);
}
You added the getItem and setItem methods of localStorage in two useEffect hooks.
The following code intializes the todo value in localStorage when reloading the page.
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}, [todos])
So you need to set the todo value in HandleAddTodo event.
I edited your code and look forward it will help you.
import React, { useState, useRef, useEffect } from 'react';
import './App.css';
import TodoList from './TodoList';
const { v4: uuidv4 } = require('uuid');
const LOCAL_STORAGE_KEY = 'todoApp.todos'
function App() {
const [todos, setTodos] = useState([]);
const TodoNameRef = useRef()
useEffect(() => {
const storageItem = localStorage.getItem(LOCAL_STORAGE_KEY);
const storedTodos = storageItem ? JSON.parse(storageItem) : [];
if (storedTodos) setTodos(storedTodos)
}, []);
function HandleAddTodo(e){
const name = TodoNameRef.current.value;
if (name==='') return;
const nextTodos = [...todos, { id:uuidv4(), name:name, complete:false}];
setTodos(nextTodos);
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(nextTodos));//replace todos to nextTodos
TodoNameRef.current.value = null
}
return (
<>
<TodoList todos={todos}/>
<input ref={TodoNameRef} type="text" />
<button onClick={HandleAddTodo}>Add todo</button>
<button>clear todo</button>
<p>0 left todo</p>
</>
)
}
export default App;
There is no need of adding the second useEffect.
You can set your local Storage while submitting in the handleTodo function.
Things you need to add or remove :
Remove the Second useEffect.
Modify your handleTodo function :
const nextTodos = [...todos, { id:uuidv4(), name:name,complete:false}];
setTodos(nextTodos);
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(nextTodos));
Note: Make sure you won't pass todos instead of nextTodos as we know setTodos is an async function There might be a chance we are setting a previous copy of todos
I am trying to deploy the react app on netlify. Locally, my react todo app is working fine and local storage is persisting the data as well (locally). but when I deployed the web app on netlify, after visiting the live link it's showing me this error: "Unexpected token u in JSON at position 0 at JSON.parse" as it is retrieving the undefined.
Solutions I tried: checked the typo
2: checked the object in the console which is displaying the data correctly.
3: i also kept the getTodoFromLocal function inside App() function and kept initial state of
const [todos, setTodos] = useState([]); (to empty array) but this is not persisting data on page reloads
my code App.js
import React, {useEffect, useState} from 'react';
import './App.css';
import { Header, Form, TodoList } from './components';
// get data from local
const getTodoFromLocal = () => {
if(localStorage.getItem("todos") === null){
localStorage.setItem("todos", JSON.stringify([]));
} else {
try{
let localTodo = localStorage.getItem("todos");
let parsedTodo = JSON.parse(localTodo)
return parsedTodo;
} catch(error) {
console.log(error);
}
}
}
const App = () => {
// states
const [inputText, setInputText] = useState("");
const [todos, setTodos] = useState(getTodoFromLocal());
const [status, setStatus] = useState("all");
const [filteredTodo, setFilteredTodo] = useState([]);
// run this only once when app loads
useEffect(() => {
getTodoFromLocal();
}, [])
// Run once when app loads and every time when there is any change in todos ot status
useEffect(() => {
filterHandler();
saveToLocal();
}, [todos, status])
// functions
const filterHandler = () =>{
switch (status) {
case 'completed':
setFilteredTodo(todos.filter(todo => todo.completed === true));
break;
case 'incompleted':
setFilteredTodo(todos.filter(todo => todo.completed === false));
break;
default:
setFilteredTodo(todos);
break;
}
}
// save to local storage / set todos;
const saveToLocal = () => {
localStorage.setItem("todos", JSON.stringify(todos));
};
return (
<div className="App">
<Header />
<Form inputText = {inputText}
setInputText={setInputText}
todos={todos}
setTodos={setTodos}
setStatus={setStatus}
/>
<TodoList
todos={todos}
setTodos={setTodos}
filteredTodo={filteredTodo}
/>
</div>
);
}
export default App;
TodoList.js
import React from 'react';
import TodoItem from './TodoItem';
const TodoList = ({todos, setTodos, filteredTodo}) => {
return (
<div className='todo-container'>
<ul className='todo-list'>
{filteredTodo && filteredTodo.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
todos={todos}
setTodos={setTodos}
text={todo.text}
/>
))}
</ul>
</div>
)
}
export default TodoList;
Form.js
import React from 'react';
import './form.css';
import { v4 as uuidv4 } from 'uuid';
function Form({inputText, setInputText, todos, setTodos, setStatus}) {
const inputHandler = (e) => {
setInputText(e.target.value);
}
const submitHandler = (e) =>{
e.preventDefault();
// generate unique id for todo lists.
const uniqueId = uuidv4();
//add todo object on click of button
const addItem = !inputText ? alert("enter somthing") : setTodos([
...todos, {id: uniqueId, text: inputText, completed: false }
]);
//reset the input field after adding todo
setInputText("");
return addItem;
}
// filtered todo
const statusTodo = (e) => {
setStatus(e.target.value);
}
return (
<form>
<input type="text" className="todo-input" onChange={inputHandler} value={inputText}/>
<button className="todo-button" type="submit" onClick={submitHandler}>
<i className="fas fa-plus-square"></i>
</button>
<div className="select">
<select onChange={statusTodo} name="todos" className="filter-todo">
<option value="all">All</option>
<option value="completed">Completed</option>
<option value="incompleted">Incompleted</option>
</select>
<span><i className="fas fa-chevron-down"></i></span>
</div>
</form>
)
}
export default Form;
TodoItem.js
import React from 'react';
import './todo.css';
const TodoItem = ({text, todo, todos, setTodos}) => {
//delete an item;
const deleteHandler = () => {
setTodos(todos.filter(el => el.id !== todo.id))
}
const completeHandler = () => {
setTodos(todos.map((item )=> {
if(item.id === todo.id){
return {
...item, completed: !todo.completed
}
}
return item;
}));
}
return (
<div className='todo'>
<li className={`todo-item ${todo.completed ? 'completed' : "" }`}>{text}</li>
<button className='complete-btn' onClick={completeHandler}>
<i className='fas fa-check'></i>
</button>
<button className='trash-btn' onClick={deleteHandler}>
<i className='fas fa-trash'></i>
</button>
</div>
)
}
export default TodoItem;
Live link: https://clinquant-parfait-ceab31.netlify.app/
Github link: https://github.com/Mehreen57/Todo-app-react
I figured the error. Going step by step.
You have getTodoFromLocal which is called when you setTodos const [todos, setTodos] = useState(getTodoFromLocal()); here.
As localStorage.getItem("todos") is null, you set todos to [] but do not return anything which returns undefined and value of todos is changed to undefined.
Then you have the function saveToLocal in which you store todos in localStorage.which is called in useEffect whenever todos change.
Todos changed to undefined > useEffect is called > in saveToLocal function todos(undefined) is stored in localStorage.
updated code:
if (localStorage.getItem("todos") === null) {
localStorage.setItem("todos", JSON.stringify([]));
return []
}
how do I get the values from the component that has the input to the component that is actually going to make the post? is that posible? or should I put all in the same component?
this is my Item component:
import React, {useState} from 'react';
import {Col} from 'reactstrap';
export function CustomFieldsItem({item}) {
const [value, setValue] = useState(false);
function handleChange(e) {
setValue(e.target.value);
}
return (
<>
<li className={'list-group-item d-flex border-0'} key={item.id}>
<Col md={2}>
<label>{item.label}</label>
</Col>
<Col md={10}>
<input className="form-control" type="text" value={value || item.value} onChange={handleChange} />
// <-- this is the value I want to pass to my update component when I type on it
// but the save button is in the "update component" (the one under)
</Col>
</li>
</>
);
}
This is my update Component:
import React, {useState, useEffect} from 'react';
import axios from 'axios';
import {post} from '../../common/fetch/fetchOptions';
export function CustomFieldsUpdate({item}) {
const [value, setValue] = useState(false);
const updateCustomField = async ({data}) => {
try {
await axios(
post({
url:'http://localhost:9000/projectcustomfields.json/update/1741?id=party&value=newvalue',
// <-- I want to set the value here, instead of "newvalue" but right now Im happy with just a log of the value from the component above in the console when I click on "Save" button.
// When I save right now I just post hardcoded values (which is what I want to change)
data: data,
method: 'POST',
mode: 'cors',
withCredentials: true,
credentials: 'include',
}),
);
console.log(value);
} catch (e) {
console.log('Error when updating values: ', e);
}
};
return (
<div className={'d-flex justify-content-end mr-4'}>
<button
type={'button'}
className={'btn btn-primary mr-2'}
onClick={updateCustomField}
>
Save
</button>
</div>
);
}
I have another component that list the objects the I want to update, maybe I need to pass the values from this component, maybe can use that?
import React, {useState, useEffect} from 'react';
import {CustomFieldsList} from './customFieldsList';
import {toast} from 'react-toastify';
import {ToastInnerDisplay} from '#learnifier/jslib-utils';
import {CustomFieldsItem} from './customFieldsItem';
export function CustomFieldsContainer({match}) {
const [value, setValue] = useState({
data: null,
loading: true,
error: null,
});
/**
* Initial loading of data.
*/
async function fetchData() {
setValue({...value, error: null, loading: true});
try {
let url = `http://localhost:9000/projectcustomfields.json/list/1741`;
const res = await fetch(url, {
method: 'POST',
mode: 'cors',
withCredentials: true,
credentials: 'include',
});
let data = await res.json();
setValue(prevValue => ({...prevValue, data: data.customFields, loading: false}));
} catch (error) {
toast.error(<ToastInnerDisplay message={error.message} />);
setValue({...value, error, loading: false});
}
}
useEffect(() => {
fetchData();
}, []);
if (value.loading) {
return <div>loading...</div>;
} else if (value.error) {
return <div>ERROR</div>;
} else {
return (
<div className={'section-component'}>
<div className={'col-md-6 col-sm-12'}>
<h2>Custom Fields</h2>
<CustomFieldsList setValue={setValue} list={value.data} />
</div>
</div>
);
// return <ChildComponent data={value.data} />
}
}
I render the components with a component list:
import React from 'react';
import {CustomFieldsItem} from './customFieldsItem';
import {CustomFieldsUpdate} from './customFieldsUpdate';
export function CustomFieldsList({list, setValue, update,
updateCustomField}) {
console.log(list);
console.log(update);
return (
<>
<form>
<ul className={'list-group border-0'}>
{list.map(item => (
<CustomFieldsItem item={item} key={item.id} />
))}
</ul>
<CustomFieldsUpdate updateCustomField={updateCustomField} setValue={setValue}/>
</form>
</>
);
}