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('')
};
Related
I am having issues with react-router#6 when implementing search parameters.
To begin with, the app can search using the search form (for example, if the user searches dark, the app would direct to localhost:3000/search?query=dark to display the results),
can also use the URL in the search bar to be directed to the right page and results (for example, if the user use the URL localhost:3000/search?query=dark, it will direct to the page and display the results). Now, the issue is when the user types in the search form, it changes the URL by adding search parameters instantly. I am aware that this is caused by the setSearchParams() in the handleChange function, but is there any way around this to NOT change the URL when typing in the search form?
import React from 'react'
import Navbar from './Navbar'
import create from 'zustand'
import { useState, useEffect } from 'react'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faTriangleExclamation } from '#fortawesome/free-solid-svg-icons'
// Zustand
let store = (set) => ({
// input: '',
// setInput: (value) => set({ input: value }),
allImages: [],
setAllImages: (images) => set({ allImages: images}),
totalResults: null,
setTotalResults: (num) => set({ totalResults: num}),
})
export const useMain = create(store)
function Header() {
const [error, setError] = useState(null)
const [showError, setShowError] = useState(false)
const [fadeOut, setFadeOut] = useState(false)
const [page, setPage] = useState(1)
const [searchParams, setSearchParams] = useSearchParams()
const query = searchParams.get('query') || ''
const allImages = useMain(state => state.allImages)
const setAllImages = useMain(state => state.setAllImages)
// const totalResults = useMain(state => state.totalResults)
const setTotalResults = useMain(state => state.setTotalResults)
function handleChange(event) {
// setInput(event.target.value)
setSearchParams({query: event.target.value})
}
async function fetchImages() {
try {
const res = await fetch(`https://api.unsplash.com/search/photos?&page=${page}&per_page=30&query=${query}&client_id=${process.env.REACT_APP_UNSPLASH_API_KEY}`)
const data = await res.json()
if (data.total !== 0) {
setAllImages(data.results)
setTotalResults(data.total)
}
} catch(error) {
setError(error)
}
}
let navigate = useNavigate()
const handleSubmit = async (event) => {
event.preventDefault()
fetchImages()
navigate(`/search?query=${query}`)
}
const location = useLocation()
useEffect(() => {
if (location.pathname === '/search' && allImages.length === 0) {
fetchImages()
navigate(`/search?query=${query}`)
}
}, [query])
// error
useEffect(() => {
if (error) {
setShowError(true)
setTimeout(() => {
setFadeOut(true)
setTimeout(() => {
setShowError(false)
}, 1000)
}, 5000)
}
}, [error])
return (
<div className='header'>
<Navbar />
<h2 className='header--heading text-center text-light'>Find Images</h2>
<div className='header--form'>
<form onSubmit={handleSubmit}>
<input
className='header--form--input'
autoComplete='off'
type='text'
placeholder='Search'
onChange={handleChange}
name='input'
value={query}
/>
</form>
</div>
{showError && <div className={`network-error ${fadeOut ? 'fade-out' : ''}`}>
<i><FontAwesomeIcon icon={faTriangleExclamation} /></i>
<div className='network-error--message'>
<h5>Network Error</h5>
<p>Please check your Internet connection and try again</p>
</div>
</div>}
</div>
)
}
export default Header
import './App.css';
import Main from './components/Main';
import Search from './components/pages/Search'
import Favorites from './components/pages/Favorites';
import Error from './components/pages/Error';
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { SkeletonTheme } from 'react-loading-skeleton';
import { useDarkMode } from './components/Navbar';
function App() {
const darkMode = useDarkMode(state => state.darkMode)
let style
if (darkMode === 'light') {
style = 'wrapper'
} else {
style = 'wrapper-dark'
}
return (
<div className={style}>
<SkeletonTheme baseColor="#808080" highlightColor="#b1b1b1">
<BrowserRouter>
<Routes>
<Route path='/' element={<Main />} />
<Route path='search' element={<Search />} />
<Route path='favorites' element={<Favorites />} />
<Route path='*' element={<Error />} />
</Routes>
</BrowserRouter>
</SkeletonTheme>
</div>
);
}
export default App;
If I'm understanding your question correctly, you don't want to update the query queryString parameter in real-time as the form field is being updated, but sometime later, like when the form is submitted. Keep in mind that the setSeaarchParams function works just like the navigate function, but operates on the queryString.
You can manually update the searchParams object, and when the form is submitted, call setSearch params instead of navigate. Remove the value prop from the input element as we'll be updating the searchParams object.
function Header() {
...
const location = useLocation()
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams()
...
function handleChange(event) {
searchParams.set("query", event.target.value);
}
...
const handleSubmit = async (event) => {
event.preventDefault();
fetchImages();
setSearchParams(searchParams);
}
...
return (
<div className='header'>
<Navbar />
<h2 className='header--heading text-center text-light'>Find Images</h2>
<div className='header--form'>
<form onSubmit={handleSubmit}>
<input
className='header--form--input'
autoComplete='off'
type='text'
placeholder='Search'
onChange={handleChange}
name='input'
/>
</form>
</div>
...
</div>
)
}
export default Header
I have a page which shows some collections from my firestore database, I am struggling to work out how to use the orderBy function to show the documents in a specific order.
I'm not sure where to put orderBy in the code. I would like to order them by a field from the firestore documents called 'section'.
I've been trying this week following other tutorials and answers from StackOverflow but can't yet work it out.
import React, { useEffect, useState, Component, setState } from 'react';
import { collection, getDocs, getDoc, doc, orderBy, query } from 'firebase/firestore';
import "./AllSections.css";
import { Firestoredb } from "../../../../../firebase.js";
import AllCourses from './AllCourses';
import ReactPlayer from 'react-player'
import ViewSection from './ViewSection';
import SectionsTabData from './SectionsTabData';
import {
BrowserRouter as Router,
Link,
Route,
Routes,
useParams,
} from "react-router-dom";
import VideoJS from './VideoJS';
function SectionsData() {
const videoJsOptions = {
controls: true,
sources: [{
src: sectionVideo,
type: 'video/mp4'
}]
}
const {courseId} = useParams();
const {collectionId} = useParams();
const params = useParams();
const [sectionId, setSectionId] = useState('');
const [sectionImage, setSectionImage] = useState('');
const [sectionVideo, setSectionVideo] = useState('');
const [sectionContent, setSectionContent] = useState('');
const [isShown, setIsShown] = useState(false);
const handleClick = event => {
// 👇️ toggle shown state
setIsShown(current => !current);
}
const [active, setActive] = useState();
const [id, setID] = useState("");
const [Sections, setCourses, error, setError] = useState([]);
useEffect(() => {
getSections()
}, [])
useEffect(() =>{
console.log(Sections)
}, [Sections])
function getSections() {
const sectionsCollectionRef = collection(Firestoredb, collectionId, courseId, 'Sections');
orderBy('section')
getDocs(sectionsCollectionRef)
.then(response => {
const content = response.docs.map(doc => ({
data: doc.data(),
id: doc.id,
}))
setCourses(content)
})
.catch(error => console.log(error.messaage))
}
const handleCheck = (id, image, video, content) => {
console.log(`key: ${id}`)
/*alert(image)*/
setSectionId(id)
setSectionImage(image)
setSectionVideo(video)
setSectionContent(content)
}
return (
<>
<div className='MainSections'>
<div className='Sidebar2'>
<ul className='SectionContainer'
>
{Sections.map(section => <li className='OneSection' key={section.id}
style={{
width: isShown ? '100%' : '200px',
height: isShown ? '100%' : '50px',
}}
onClick={() =>
handleCheck(section.id, section.data.thumbnailImageURLString, section.data.videoURLString, section.data.contentURLString)}
id = {section.id}
>
<br />
{section.data.name}
<br />
<br />
{isShown && (
<img className='SectionImage' src={section.data.thumbnailImageURLString !== "" ? (section.data.thumbnailImageURLString) : null} alt='section image'></img>
)}
<br />
</li>)}
</ul>
</div>
<div className='ViewSection'>
<iframe className='Content' src={sectionContent}
width="100%"/>
</div>
</div>
</>
)
}
export default SectionsData
You are using orderBy incorrectly please view the docs here: https://firebase.google.com/docs/firestore/query-data/order-limit-data
Your query should look something along these lines if you're trying to order your data in a specific way. Assuming your sectionsCollectionRef is correct:
const sectionsCollectionRef = collection(Firestoredb, collectionId, courseId, 'Sections')
const q = query(sectionsCollectionRef, orderBy('section', 'desc'))
const querySnapshot = await getDocs(q);
The orderBy() won't do anything on it's own. You must use it along query() function to add the required QueryConstraint and build a Query as shown below:
import { collection, query } from "firebase/firestore"
const sectionsCollectionRef = collection(Firestoredb, collectionId, courseId, 'Sections');
const sectionsQueryRef = query(sectionsCollectionRef, orderBy("section"))
i'm new and i'm trying to learn react and redux, I was trying to do this simple user list / todo list, I can add list items but I can't remove them with the remove in the reducer, if i click on the button it deletes all the array and the log of payload returns undefined, can someone help me? thanks all for the help!
STORE
import { combineReducers } from "#reduxjs/toolkit";
import { userState } from "./UsersState";
import { createStore } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
const rootReducer = combineReducers({
users: userState.reducer,
});
export const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
REDUCER
import { createSlice, current } from "#reduxjs/toolkit";
export const userState = createSlice({
name: "users",
initialState: [],
reducers: {
add: (state, action) => [...state, action.payload],
remove: (state, action) =>
state.filter((user, index) => user.id !== action.payload),
clear: (state, action) => [],
log: (state, action) => console.log(state),
},
});
RENDER
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { store } from "./Store";
import { add, remove, clear, userState } from "./UsersState";
export function UsersList() {
const users = useSelector((state) => state.users);
const dispatch = useDispatch();
function addUser(event) {
event.preventDefault();
dispatch(userState.actions.add(event.target.elements.username.value));
dispatch(userState.actions.log());
}
function removeUser() {
dispatch(userState.actions.remove());
}
function clearList() {
dispatch(userState.actions.clear());
console.log(store.getState());
}
function logState() {
dispatch(userState.actions.log());
}
return (
<div>
<ul className="ul">
{users.map((user, index) => (
<div className="list-element" key={index + 1}>
<li key={index}>{user}</li>
<button onClick={removeUser} type="submit">
remove
</button>
</div>
))}
</ul>
<form onSubmit={addUser}>
<input name="username" type="text" />
Insert name
<button type="submit">Add user</button>
</form>
<button onClick={clearList}>Clear list</button>
<button onClick={logState}>Log state</button>
</div>
);
}
First of all, update the component like following (I commented on the place of change)
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { store } from "./Store";
import { add, remove, clear, userState } from "./UsersState";
// make sure to export these actions from the reducer file
export function UsersList() {
const users = useSelector((state) => state.users);
const dispatch = useDispatch();
function addUser(event) {
event.preventDefault();
dispatch(userState.actions.add(event.target.elements.username.value));
dispatch(userState.actions.log());
}
function removeUser() {
dispatch(userState.actions.remove());
}
function clearList() {
dispatch(userState.actions.clear());
console.log(store.getState());
}
function logState() {
dispatch(userState.actions.log());
}
return (
<div>
<ul className="ul">
{users.map((user, index) => (
<div className="list-element" key={index + 1}>
<li key={index}>{user}</li>
{/* you can directly dispatch the remove function*/}
{/* no need this button to be type="button"*/}
{/* change here */}
<button onClick={() => dispatch(remove(user))}>
remove
</button>
</div>
))}
</ul>
<form onSubmit={addUser}>
<input name="username" type="text" />
Insert name
<button>Add user</button>
</form>
<button onClick={clearList}>Clear list</button>
<button onClick={logState}>Log state</button>
</div>
);
}
You needed to pass user in the action and dispatch it, hope it helps!
For the reducer file, you have to export the actions also
import { createSlice, current } from "#reduxjs/toolkit";
export const userState = createSlice({
name: "users",
initialState: [],
reducers: {
add: (state, action) => [...state, action.payload],
remove: (state, action) =>
// need to redefine the state, forgot this part
// change here
state = state.filter((user, index) => user !== action.payload),
clear: (state, action) => [],
log: (state, action) => console.log(state),
},
});
export const {add, remove, clear, log} = userState.actions;
export default userState.reducer;
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
}
...
}
I would to add geolocation.getCurrentPosition in my code below using react and redux and then send the coordinates (Lat and Long) to the Local Storage but i don't have any idea !
I deleted the HTML parts of the form (city, postalCode .. to avoid repetition
Anyone can help me with please
import React, { useState } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import FormContainer from '../components/FormContainer';
import CheckoutSteps from '../components/CheckoutSteps';
import { saveShippingAddress } from '../actions/cartActions';
const ShippingScreen = ({ history }) => {
const cart = useSelector((state) => state.cart);
const { shippingAddress } = cart;
const [address, setAddress] = useState(shippingAddress.address);
const [city, setCity] = useState(shippingAddress.city);
const [postalCode, setPostalCode] = useState(shippingAddress.postalCode);
const [country, setCountry] = useState(shippingAddress.country);
const dispatch = useDispatch();
const submitHandler = (e) => {
e.preventDefault();
dispatch(
saveShippingAddress({ address, city, postalCode, country })
);
history.push('/payment');
};
return (
<FormContainer>
<CheckoutSteps step1 step2 />
<h1>Shipping</h1>
<Form onSubmit={submitHandler}>
<Form.Group controlId="address">
<Form.Label>Address</Form.Label>
<Form.Control
type="text"
placeholder="Enter address"
value={address}
required
onChange={(e) => setAddress(e.target.value)}
></Form.Control>
</Form.Group>
<Button type="submit" variant="primary">
Continue
</Button>
</Form>
</FormContainer>
);
};
export default ShippingScreen;