I created a search component to get the cocktails by name, but I want to add another search option based on a checkbox(so the cocktail is alcoholically or not).
I have a context.js file:
import React, { useState, useContext, useEffect } from 'react'
import { useCallback } from 'react'
const url = 'https://www.thecocktaildb.com/api/json/v1/1/search.php?s='
const AppContext = React.createContext()
const AppProvider = ({ children }) => {
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('a')
const [searchCheckbox, setSearchCheckbox] = useState(false)
const [cocktails, setCocktails] = useState([])
const fetchDrinks = useCallback(async () => {
setLoading(true)
setSearchCheckbox(false)
try {
const response = await fetch(`${url}${searchTerm}`)
const data = await response.json()
const {drinks} = data
if(!drinks) {
setCocktails([])
} else {
const searchedCocktails = drinks.map((drink) => {
const {idDrink, strDrink, strDrinkThumb, strInstructions,strAlcoholic, strIngredient1,strIngredient2} = drink
return {
id: idDrink,
name: strDrink,
image: strDrinkThumb,
isAlcoholic: strAlcoholic,
info: strInstructions,
ingredient1: strIngredient1,
ingredient2: strIngredient2
}
})
setCocktails(searchedCocktails)
}
setLoading(false)
} catch (error) {
console.log(error)
setLoading(false)
}
}, [searchTerm])
useEffect(() => {
fetchDrinks()
}, [searchTerm, fetchDrinks])
return <AppContext.Provider
value={{loading,
cocktails,
setSearchTerm,
setSearchCheckbox
}}>
{children}
</AppContext.Provider>
}
export const useGlobalContext = () => {
return useContext(AppContext)
}
export { AppContext, AppProvider }
The searchbar component is the following:
import React from 'react'
import { useGlobalContext } from '../helpers/context'
export default function SearchBar() {
const searchValue = React.useRef('')
const searchCheckbox = React.useRef(false)
const {setSearchTerm} = useGlobalContext()
const {setSearchCheckbox} = useGlobalContext()
const searchCocktail = () => {
setSearchTerm(searchValue.current.value)
setSearchCheckbox(searchCheckbox.current.checked)
}
const handleSubmit = (e) =>{
e.preventDefault()
}
//setup auto focus on input
React.useEffect(() => {
searchValue.current.focus()
searchCheckbox.current.focus()
}, [])
return (
<div className="container">
<div className="row">
<div className="col-12">
<div className="input-group">
<input className="form-control border-secondary py-2" type="search" ref={searchValue} onChange={searchCocktail}/>
<div className="input-group-append">
<button onClick={handleSubmit} className="btn btn-outline-secondary" type="button">
<i className="fa fa-search"></i>
</button>
</div>
</div>
</div>
<div className="col-12">
<div className="form-check">
<input className="form-check-input" type="checkbox" ref={searchCheckbox} onChange={searchCocktail} id="flexCheckDefault"/>
<label onClick={handleSubmit} className="form-check-label" htmlFor="flexCheckDefault">
Alcoholic
</label>
</div>
</div>
</div>
</div>
)
}
Can somebody can path me to a way to solve my problem? I haven't coded in React for some time and I think I m doing something hugely wrong with the useState hook on the checkbox
You need to create reference to state in parent element and pass it to child.
const [ alcoholicFilter, setAlcoholicFilter ] = useState(false) // false is default value
// here you can use `alcoholicFilter` variable and it will be updated
...
<Searchbar filter={setAlcoholicFilter}/>
After that in Searchbar component you can use this reference to update parent state
export default (props) => {
...
const handleSubmit = () => {
...
props.filter(input.checked) // you can set here to whatever you need
}
...
}
Related
I have React components :
Main.jsx
import { useState, useEffect } from "react";
import { Preloader } from "../Preloader";
import { Pokemons } from "../Pokemons";
import { LoadMore } from "../LoadMore";
function Main() {
const [pokemons, setPokemons] = useState([]);
const [loading, setLoading] = useState(true);
const [pokemonsPerPage] = useState("20");
const [pokemonOffset] = useState("0");
useEffect(function getPokemons() {
fetch(
`https://pokeapi.co/api/v2/pokemon?limit=${pokemonsPerPage}&offset=${pokemonOffset}`
)
.then((responce) => responce.json())
.then((data) => {
data.results && setPokemons(data.results);
setLoading(false);
});
}, []);
return (
<main className="container content">
{loading ? <Preloader /> : <Pokemons pokemons={pokemons} />}
<LoadMore />
</main>
);
}
export { Main };
LoadMore.jsx
import React from 'react'
function LoadMore() {
return (
<div className="button_container">
<a class="waves-effect waves-light btn-large" id="more">
More...
</a>
</div>
);
}
export { LoadMore };
I have created a button in the component. After clicking on it, the next 20 elements should be loaded. I created const [pokemonsPerPage] = useState("20"); and const [pokemonOffset] = useState("0"); in order to substitute these values into the request. The first is responsible for the number of objects on the page, the second is responsible for which element to start counting from. That is, now 20 elements are being output, starting from the very first one. (If you change const [pokemonOffset] = useState("0"); to 20, then the output will start from 21 elements). I'm sure that this is necessary for implementation, but I don't know what to do next
Help me complete this functionality
Here is what your Main.jsx should look like.
import { useState, useEffect } from "react";
import { Preloader } from "../Preloader";
import { Pokemons } from "../Pokemons";
import { LoadMore } from "../LoadMore";
function Main() {
const [pokemons, setPokemons] = useState([]);
const [loading, setLoading] = useState(true);
const [pokemonsPerPage] = useState(20);
const [page,setPage] = useState(1);
function getPokemons(pokemonOffset) {
fetch(
`https://pokeapi.co/api/v2/pokemon?limit=${pokemonsPerPage}&offset=${pokemonOffset}`
)
.then((responce) => responce.json())
.then((data) => {
data.results && setPokemons(data.results);
setLoading(false);
});
}
useEffect(() => {
const offset= page*pokemonsPerPage -pokemonsPerPage;
getPokemons(offset);
}, [page]);
return (
<main className="container content">
{loading ? <Preloader /> : <Pokemons pokemons={pokemons} />}
<LoadMore next={()=>setPage(p=>p+1)} />
</main>
);
}
export { Main };
while your Loadmore.jsx
import React from 'react'
function LoadMore(next) {
return (
<div className="button_container">
<a type="button" onclick={next} class="waves-effect waves-light btn-large" id="more">
More...
</a>
</div>
);
}
export { LoadMore };
I'm developing a CRUD and, in one of the components, when the user creates the post, a div is rendered with the title and content values of his post. I need his posts to be saved even when he navigates between pages (without refreshing the page). Currently I can create as many posts as I want and they will be lined up one below the other, but when I go back to the previous page they are deleted. I tried to implement redux in this part the way I implemented it to save the user input (creating a slice to save the posts) but it didn't work. I know normally posts wouldn't be deleted, so I'd like to know where I'm going wrong.
signup screen:
import React, {useState, useEffect} from "react";
import "../_assets/signup.css";
import "../_assets/App.css";
import { useDispatch } from 'react-redux';
import userSlice from '../redux/userslice';
import { useNavigate } from "react-router-dom";
function Signup() {
const navigate = useNavigate();
const dispatch = useDispatch();
const [name, setName] = useState('')
const [buttonGrey, setButtonGrey] = useState('#cccccc')
useEffect(() => {
if (name!== '') {
setButtonGrey("black")
}
else {
setButtonGrey('#cccccc')
}
}, [name])
const handleSubmitForm= (e) => {
e.preventDefault()
dispatch(userSlice.actions.saveUser(name))
navigate("/main")
}
const handleChangeName = (text) => {
setName(text)
}
return (
<div className="container">
<div className="LoginBox">
<form onSubmit={handleSubmitForm}>
<h2>Welcome to codeleap network</h2>
<text>Please enter your username</text>
<input type="text" name="name" value={name} onChange = {e => handleChangeName(e.target.value)} placeholder="Jane Doe" />
<div className="button">
<button type="submit" style={{backgroundColor: buttonGrey}} disabled={!name} >
ENTER
</button>
</div>
</form>
</div>
</div>
);
}
export default Signup;
CRUD screen:
import React, { useState, useEffect } from "react";
import "../_assets/App.css";
import "../_assets/mainscreen.css";
import { MdDeleteForever } from "react-icons/md";
import { FiEdit } from "react-icons/fi";
import { useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { Navigate } from 'react-router-dom';
import postsSlice from '../redux/postsslice'
import Modal from "../components/modal.jsx";
function MainScreen() {
const dispatch = useDispatch();
const user = useSelector((state) => state.user)
const loadPosts = useSelector((state) => state.loadPosts)
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [newPosts, setNewPosts] = useState([])
const [buttonGreyOut, setButtonGreyOut] = useState("#cccccc");
useEffect(() => {
if (title && content !== "") {
setButtonGreyOut("black");
} else {
setButtonGreyOut("#cccccc");
}
},[title, content]);
const handleSubmitSendPost = (e) => {
e.preventDefault();
setNewPosts(newPosts.concat({title, content}))
dispatch(postsSlice.actions.savePosts(newPosts))
setTitle('')
setContent('')
};
const handleChangeTitle = (text) => {
setTitle(text);
};
const handleChangeContent = (text) => {
setContent(text);
};
const [openModal, setOpenModal] = useState();
if (user === '') {
return <Navigate to="/" />
} else {
return (
<div className="containerMainScreen">
{openModal && <Modal closeModal={setOpenModal} />}
<div className="bar">
<h1>Codeleap</h1>
</div>
<div className="boxPost">
<h2 style={{ fontWeight: 700 }}>What's on your mind?</h2>
<h2>Title</h2>
<form onSubmit={handleSubmitSendPost}>
<input
type="text"
placeholder="Hello World"
name="name"
value={title}
onChange={(e) => handleChangeTitle(e.target.value)}
></input>
<h2>Content</h2>
<textarea
placeholder="Content"
name="content"
value={content}
onChange={(e) => handleChangeContent(e.target.value)}
></textarea>
<button
className="createButton"
type="submit"
style={{ backgroundColor: buttonGreyOut }}
disabled={!title || !content}
>
CREATE
</button>
</form>
</div>
{newPosts.map((post) => (
<div className="boxPost">
<div className="bar">
<h1>{post.title}</h1>
<MdDeleteForever
className="icon"
onClick={() => {
setOpenModal(true);
}}
/>
<FiEdit
style={{ color: "white", fontSize: "45px", paddingLeft: "23px" }}
/>
</div>
<div id="postowner">
<h3>#{user}</h3>
<h3>25 minutes ago</h3>
<br></br>
<textarea style={{ border: "none" }}>{post.content}</textarea>
</div>
</div>
))}
</div>
);
}
}export default MainScreen;
store.js:
import { configureStore } from '#reduxjs/toolkit';
import userSlice from './userslice';
import postsSlice from './postsslice'
export const store = configureStore({
reducer: {
user: userSlice.reducer,
loadPosts: postsSlice.reducer
},
})
postsSlice:
import { createSlice } from "#reduxjs/toolkit";
const postsSlice = createSlice({
name: "posts",
initialState: "",
reducers: {
savePosts: (state, action) => action.payload
}
});
export default postsSlice
CRUD screenshot:
https://i.stack.imgur.com/YoCJz.png
You are dispatching the savePosts action with the value of newPosts from before the current posts are added to it. The problem is in these lines:
setNewPosts(newPosts.concat({title, content}))
dispatch(postsSlice.actions.savePosts(newPosts))
Try something like this instead:
const posts = newPosts.concat({title, content})
setNewPosts(posts)
dispatch(postsSlice.actions.savePosts(posts))
It does not make sense to store the posts array in Redux and also store it in local component state. In my opinion you should ditch the component newPosts state and access the posts via Redux with useSelector.
I would also recommend dispatching an addPost action which requires just the current post. Let the reducer handle adding it to the array.
The state is an array of posts, so your initialState should be an empty array rather than an empty string.
const postsSlice = createSlice({
name: "posts",
initialState: [],
reducers: {
// add one post to the array.
addPost: (state, action) => {
state.push(action.payload); // modifies the draft state.
},
// replace the entire array.
replacePosts: (state, action) => action.payload
}
});
function MainScreen() {
const dispatch = useDispatch();
const user = useSelector((state) => state.user)
const posts = useSelector((state) => state.loadPosts)
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
/* ... */
const handleSubmitSendPost = (e) => {
e.preventDefault();
dispatch(postsSlice.actions.addPost({title, content}))
setTitle('')
setContent('')
};
I have this issue that I'm trying to fix, I have this piece of code that allows me to create a new blog:
import React, { useState, useEffect, useRef } from 'react'
import Blog from './components/Blog'
import Notification from './components/Notification'
import LoginForm from './components/LoginForm'
import BlogForm from './components/BlogForm'
import Togglable from './components/Togglabe'
import blogService from './services/blogs'
import loginService from './services/login'
import './App.css'
const App = () => {
const [blogs, setBlogs] = useState([])
const [user, setUser] = useState(null)
const [errorMessage, setErrorMessage] = useState(null)
const [showAll, setShowAll] = useState(true)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const blogFormRef = useRef()
useEffect(() => {
blogService
.getAll()
.then(initialBlogs => {
setBlogs(initialBlogs)
})
}, [])
// ----- Add blog function ------
const addBlog = async (blogObject) => {
blogFormRef.current.toggleVisibility()
const toAdd = blogService.createBlog(blogObject)
const returned = await toAdd
setBlogs(blogs.concat(returned))
}
// ----- Update Blog -----
const updateBlog = async (id) => {
const blog = blogs.find(blog => blog.id === id)
const toUpdate = { ...blog, likes: blog.likes + 1 }
const updateo = blogService.update(id, toUpdate)
const returned = await updateo
setBlogs(blogs.map(blog => blog.id !== id ? blog : returned))
}
// ----- Delete Blog -----
const delBlog = async (id) => {
const blogToDelete = blogService.deleteBlog(id)
const deleted = await blogToDelete
return deleted
}
// ----- Forms -------
const blogForm = () => (
<Togglable buttonLabel='new blog' ref={blogFormRef}>
<BlogForm
createBlog={addBlog}
/>
</Togglable>
)
return (
<div className="App">
<h2>Blogs</h2>
<Notification message={errorMessage}/>
{user === null ?
loginForm() :
<div>
<p>
{user.name} logged-in
<button onClick={(() => logOut())}>Log out</button>
</p>
{blogForm()}
</div>
}
<div>
<button onClick={() => setShowAll(!showAll)}>
{showAll ? 'hide' : 'show'}
</button>
</div>
<ul>
{showAll === true ?
blogs.map((blog, i) =>
<Blog
key={i}
blog={blog}
updateBlog={updateBlog}
delBlog={delBlog} />
)
: null
}
</ul>
</div>
)
}
export default App
and this other part is the form of the app that allows you to add a new blog
import React, { useState } from 'react'
const BlogForm = ({ createBlog }) => {
const [newTitle, setNewTitle] = useState('')
const [newAuthor, setNewAuthor] = useState('')
const [newContent, setNewContent] = useState('')
const handleTitle = (event) => {
setNewTitle(event.target.value)
}
const handleAuthor = (event) => {
setNewAuthor(event.target.value)
}
const handleContent = (event) => {
setNewContent(event.target.value)
}
const addBlog = (event) => {
event.preventDefault()
createBlog({
title: newTitle,
author: newAuthor,
content: newContent,
likes: 0
})
setNewTitle('')
setNewAuthor('')
setNewContent('')
}
return (
<div className="formDiv">
<form onSubmit={addBlog}>
<div>
<div>
Title:
<input
id="title"
type="text"
value={newTitle}
onChange={handleTitle}
/>
</div>
<div>
Author:
<input
type="text"
value={newAuthor}
onChange={handleAuthor}
/>
</div>
<div>
Content:
<input
type="text"
value={newContent}
onChange={handleContent}
/>
</div>
<button type="submit">Add</button>
</div>
</form>
</div>
)
}
export default BlogForm
So the problem is that I'm trying to add a new blog but it doesn't rerender the component and it doesn't show the new blog instantly. I have to refresh the page and then it appears. The funny thing is that it works with the updateBlog component which allows you to give a "LIKE" to some blog, but it doesn't react instantly with the AddBlog component.
I'm stuck trying to understand why my state won't update until I change the value in the text input twice (calling the handleChange function). What am I doing wrong here?
import React, {useEffect, useState} from "react";
export default function Typeahead(props){
const {list} = props;
const [colorList] = useState(list.map(element => element.toLowerCase()));
const [color,setColor] = useState();
const [showResults, setShowResults]= useState(false);
const [results,setResults]= useState();
let handleChange = (e) =>{
setShowResults(true);
setColor(e.target.value.toLowerCase());
const match = (colorList) =>{
return colorList.startsWith(color,0);
};
const matches = colorList.filter(match);
setResults((matches));
console.log(results);
console.log(showResults);
};
useEffect(() => {
//setResults(list.map(elements => elements.toLowerCase()));
}, [results]);
return(
<div>
<input type= "text" onChange={handleChange}/>
{showResults ?
<div>
{results.map((options) => {
return (
<option key={options} value={options}> {options}</option>
)
})}
</div>
: null }
</div>
);
}
I'm currently struggling with react inputs and debounce from lodash.
Most of the time when I have a form I also have an edit option, so I need a controlled component to fill back the inputs using value={state["targetValue"]} so I can fill and edit the field.
However, if the component is controlled debounce isn't working.
I made a simple example on CodeSandbox: https://codesandbox.io/embed/icy-cloud-ydzj2?fontsize=14&hidenavigation=1&theme=dark
Code:
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { debounce } from "lodash";
import "./styles.css";
class App extends Component {
constructor(props) {
super(props);
this.state = {
name: "",
title: "",
editMode: false
};
this.debouncedEvent = React.createRef();
}
debounceEvent(_fn, timer = 500, options = null) {
this.debouncedEvent.current = debounce(_fn, timer, options);
return e => {
e.persist();
return this.debouncedEvent.current(e);
};
}
componentWillUnmount() {
this.debouncedEvent.current.cancel();
}
onChangeValue = event => {
const { name, value } = event.target;
this.setState(() => {
return { [name]: value };
});
};
onRequestEdit = () => {
this.setState({ name: "Abla blabla bla", editMode: true });
};
onCancelEdit = () => {
if (this.state.editMode) this.setState({ name: "", editMode: false });
};
onSubmit = event => {
event.preventDefault();
console.log("Submiting", this.state.name);
};
render() {
const { name, editMode } = this.state;
const isSubmitOrEditLabel = editMode ? `Edit` : "Submit";
console.log("rendering", name);
return (
<div className="App">
<h1> How to debounce controlled input ?</h1>
<button type="button" onClick={this.onRequestEdit}>
Fill with dummy data
</button>
<button type="button" onClick={this.onCancelEdit}>
Cancel Edit Mode
</button>
<div style={{ marginTop: "25px" }}>
<label>
Controlled / Can be used for editing but not with debounce
</label>
<form onSubmit={this.onSubmit}>
<input
required
type="text"
name="name"
value={name}
placeholder="type something"
// onChange={this.onChangeValue}
onChange={this.debounceEvent(this.onChangeValue)}
/>
<button type="submit">{isSubmitOrEditLabel}</button>
</form>
</div>
<div style={{ marginTop: "25px" }}>
<label> Uncontrolled / Can't be used for editing </label>
<form onSubmit={this.onSubmit}>
<input
required
type="text"
name="name"
placeholder="type something"
onChange={this.debounceEvent(this.onChangeValue)}
/>
<button type="submit">{isSubmitOrEditLabel}</button>
</form>
</div>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
At first it looks impossible, but there is a simple way to do it.
Just create the debounce function expression outside the react component.
Here is a pseudo example, for modern React with Hooks:
import React, { useState, useEffect } from "react";
import { debounce } from "lodash";
...
const getSearchResults = debounce((value, dispatch) => {
dispatch(getDataFromAPI(value));
}, 800);
const SearchData = () => {
const [inputValue, setInputValue] = useState("");
...
useEffect(() => {
getSearchResults(inputValue, dispatch);
}, [inputValue]);
...
return (
<>
<input
type="text"
placeholder="Search..."
value={inputValue}
onChange={e => setInputValue(e.target.value)}
/>
...
</>
);
};
export default SearchData;
Update: In time I came up with a better solution using "useCallback"
import React, { useState, useEffect, useCallback } from "react";
import { debounce } from "lodash";
...
const SearchData = () => {
const [inputValue, setInputValue] = useState("");
...
const getSearchResults = useCallback(
debounce(value => {
dispatch(getDataFromAPI(value));
}, 800),
[]
);
useEffect(() => {
getSearchResults(inputValue);
}, [inputValue]);
...
return (
<>
<input
type="text"
placeholder="Search..."
value={inputValue}
onChange={e => setInputValue(e.target.value)}
/>
...
</>
);
};
So... Apparently, there's no solution. the input takes the value from the state. While debounce prevents the state to trigger.
I made a workaround using ReactDOM.
import ReactDOM from "react-dom";
export const setFormDefaultValue = (obj, ref) => {
if (ref && !ref.current) return;
if (!obj || !obj instanceof Object) return;
const _this = [
...ReactDOM.findDOMNode(ref.current).getElementsByClassName("form-control")
];
if (_this.length > 0) {
_this.forEach(el => {
if (el.name in obj) el.value = obj[el.name];
else console.error(`Object value for ${el.name} is missing...`);
});
}
};
and then the use:
this.refForm = React.createRef();
setFormDefaultValue(this.state, refForm)
This way I can fill my form with the state default value and continue using debounce.
Take a look at this lib: https://www.npmjs.com/package/use-debounce
Here's an example of how to use it:
import React, { useState } from 'react';
import { useDebounce } from 'use-debounce';
export default function Input() {
const [text, setText] = useState('Hello');
const [value] = useDebounce(text, 1000);
return (
<div>
<input
defaultValue={'Hello'}
onChange={(e) => {
setText(e.target.value);
}}
/>
<p>Actual value: {text}</p>
<p>Debounce value: {value}</p>
</div>
);
}
You can try this.
import React, { useState, useCallback, useRef, useEffect } from 'react';
import _ from 'lodash';
function usePrevious(value: any) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const DeboucnedInput = React.memo(({ value, onChange }) => {
const [localValue, setLocalValue] = useState('');
const prevValue = usePrevious(value);
const ref = useRef();
ref.current = _.debounce(onChange, 500);
useEffect(() => {
if (!_.isNil(value) && prevValue !== value && localValue !== value) {
setLocalValue(value);
}
}, [value]);
const debounceChange = useCallback(
_.debounce(nextValue => {
onChange(nextValue);
}, 1000),
[]
);
const handleSearch = useCallback(
nextValue => {
if (nextValue !== localValue) {
setLocalValue(nextValue);
debounceChange(nextValue);
}
},
[localValue, debounceChange]
);
return (
<input
type="text"
value={localValue}
onChange={handleSearch}
/>
);
});
I had a similar issue, useMemo and debounce from lodash seem to work for me.
import React, { useState, useEffect, useMemo } from 'react';
import { debounce } from 'lodash';
const Search = () => {
const [inputValue, setInputValue] = useState('');
const handleChange = (event) => {
setInputValue(event.target.value);
};
const handleFetch = (input) => {
// do stg
};
const debouncedFetch = useMemo(() => debounce(handleFetch, 500), []);
useEffect(() => {
debouncedFetch(inputValue);
}, [inputValue]);
return (
<Form>
<input
type="text"
value={inputValue}
onChange={handleChange}
/>
// rest of the component
</Form>
);
};
Some articles that helped me:
https://dev.to/alexdrocks/using-lodash-debounce-with-react-hooks-for-an-async-data-fetching-input-2p4g
https://www.carlrippon.com/using-lodash-debounce-with-react-and-ts/
https://blog.logrocket.com/how-and-when-to-debounce-or-throttle-in-react/