I'm making a todo list app using react and firebase realtime database.
I want to get the todos ordered by date.
My Database:
And if I cant do this from firebase, is there a way to order it from the client side (react)?
My Code
Todos.js:
import { useState, useEffect } from "react";
import { signOut, onAuthStateChanged } from "firebase/auth";
import { uid } from "uid";
import { set, ref, onValue } from "firebase/database";
import { auth, db } from "../firebase";
import moment from "moment";
function Todos() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState("");
const navigate = useNavigate();
useEffect(() => {
auth.onAuthStateChanged((user) => {
if (user) {
onValue(ref(db, `/${auth.currentUser.uid}`), (snapshot) => {
setTodos([]);
const data = snapshot.val();
if (data !== null) {
Object.values(data).map((todo) => {
setTodos((currentTodos) => [todo, ...currentTodos]);
});
}
});
} else {
navigate("/");
}
});
}, []);
const handleSignOut = () => {
signOut(auth)
.then(() => navigate("/"))
.catch((error) => alert(error.message));
};
const addTodo = () => {
const uidd = uid();
set(ref(db, `${auth.currentUser.uid}/${uidd}`), {
task: newTodo,
uid: uidd,
createdAt: moment().format("YYYY-MM-DD k:m:s"),
});
setNewTodo("");
};
return (
<>
<Center>
<Button colorScheme="red" marginTop={5} onClick={handleSignOut}>
Logout
</Button>
</Center>
<Container
maxW="4xl"
marginTop={8}
display="flex"
alignItems="center"
justifyContent="center"
>
<Box
boxShadow="base"
rounded="lg"
padding={10}
background="white"
width="100%"
>
<Heading as="h1" size="md" textAlign="center">
Todo List App
</Heading>
<form onSubmit={(e) => e.preventDefault()}>
<Box
display="flex"
alignItems="center"
justifyContent="space-between"
marginTop={5}
>
<Input
placeholder="New Task"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
size="lg"
width="80%"
/>
<Button
colorScheme="teal"
height={45}
rightIcon={<MdAdd />}
margin={0}
onClick={addTodo}
type="submit"
>
Add
</Button>
</Box>
</form>
{todos.map((todo, index) => {
return <Todo key={index} task={todo.task} uid={todo.uid} />;
})}
</Box>
</Container>
</>
);
}
export default Todos;
Since you are loading the TODOs for a single user, you can indeed order them by their createdAt property. To do this, use a query as shown in the documentation on ordering and filtering data:
const ref = ref(db, `/${auth.currentUser.uid}`);
const query = query(ref, orderByChild('createdAt'));
onValue(query, (snapshot) => {
...
Inside the code you'll then need to make sure to use snapshot.forEach to loop over the children in order, as calling .val() before that will return a JSON object and the properties in a JSON object are by definition not ordered:
snapshot.forEach((child) => {
console.log(child.key, child.val());
});
Related
I want to create a todo, but after create a todo I want to not refresh all the list, but when I create a post request it also creates refresh and get request
const App = () => {
const dispatch = useDispatch()
const [hamburgerMenu, setHamburgerMenu] = useState(false);
const { authReducer } = useSelector(state => state)
useEffect(() => {
dispatch(refreshToken())
}, [dispatch])
return (
<Router>
<Alert />
<Navbar
hamburgerMenu={hamburgerMenu}
setHamburgerMenu={setHamburgerMenu}
/>
<Menu
hamburgerMenu={hamburgerMenu}
setHamburgerMenu={setHamburgerMenu}
/>
<Routes>
<Route exact path="/login" element={<Login />} />
<Route exact path="/register" element={<Register />} />
{<Route exact path="/" element={!authReducer?.access_token ? <HomepageLogout/> : <Homepage/>} /> }
</Routes>
</Router>
)
}
export default App
import { useEffect } from "react"
import TodoList from "../components/TodoList"
import { useSelector, useDispatch } from "react-redux"
import { getTodos } from "../redux/actions/todoAction"
import './styles/homepage.scss'
const Homepage = () => {
const dispatch = useDispatch()
const { authReducer, todoReducer } = useSelector((state) => state)
const userId = authReducer?.user?.userId
const access_token = authReducer?.access_token
useEffect(() => {
userId && dispatch(getTodos(userId))
}, [userId, dispatch ]);
const todoList = todoReducer?.data
return (
<div style={{ width: "100%", height: "100vh", display: 'flex', flexDirection: 'column', alignItems: "center", justifyContent: "center" }}>
<TodoList todoList={todoList} access_token={access_token} />
</div>
)
}
export default Homepage
import Todo from "./Todo"
import "./styles/todoList.scss"
import TodoForm from "./TodoForm"
const TodoList = ({ todoList, access_token, setIsAdded }) => {
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div className="todoListContainer">
<h1 className="title2">What are you planing for today?</h1>
<ul className="todoListContainer__listContainer">
{todoList &&
todoList?.map((todo) => {
return (
<Todo todoList={todoList} todo={todo} key={todo?.todoId} access_token={access_token} />)
})
}
</ul>
<TodoForm todoList={todoList} />
</div>
</div>
)
}
export default TodoList
import { useState } from 'react'
import { useSelector, useDispatch } from "react-redux"
import { createTodo } from "../redux/actions/todoAction"
import "./styles/todoForm.scss"
const TodoForm = ({ todoList }) => {
const dispatch = useDispatch()
const { authReducer } = useSelector(state => state)
const token = authReducer?.access_token
const userId = authReducer?.user?.userId
const initialState = {
text: "",
todoUserId: userId
}
const handleSubmit = (e) => {
userId && dispatch(createTodo(todo, token, todoList))
}
const handleChange = (e) => {
const { value, name } = e.target
setTodo({
...todo,
[name]: value
})
}
const [todo, setTodo] = useState(initialState)
return (
<form className="todoForm" onSubmit={handleSubmit}>
<input
placeholder="Please add something.."
id="text"
name="text"
type="text"
value={todo?.text}
onChange={handleChange}
/>
<input
id="todoUserId"
name="todoUserId"
value={todo?.todoUserId}
readOnly
style={{ display: 'none' }}
/>
<button type="submit">
Add
</button>
</form>
)
}
export default TodoForm
TODO ACTION
export const createTodo = (todo, token, todoList) => async (dispatch) => {
try {
const res = await postAPI("/todo/create", todo, token);
dispatch({
type: TODO,
payload: {
status: "success",
message: "All todos found",
data: todoList.append(todo)
}
})
dispatch({ type: ALERT, payload: { success: res.data.message } })
} catch (error) {
dispatch({ type: ALERT, payload: { errors: error?.response?.data.message } })
}
};
POSTAPI
export const postAPI = async (url, post, token) => {
const res = await axios.post(`/api/${url}`, post, {
headers: { token: token }
})
return res
}
I have tried todoList with useState() also different array methods in payload in action but still it creates get and refresh request. It is bad for me because when I create a todo, access_token becomes for a second undefined, so the list disappears and comes another component. I want to add todo under the list without refreshing the page
Here my logs after create a todo
POST /api/todo/create 200 286.656 ms - 249
GET /api/refresh_token 200 200.426 ms - 408
GET /api/refresh_token 304 3.712 ms - -
GET /api/todo/get/emrekrt163395 200 4.840 ms - 7491
GET /api/todo/get/emrekrt163395 304 10.143 ms - -
It seems the page is reloading because the form is being submitted when clicking the "add" button. The default form action is not being prevented. handleSubmit should call preventDefault on the passed onSubmit event object to prevent the default form action from submitting the form and reloading the page (and entire React application).
Example:
const handleSubmit = (e) => {
e.preventDefault(); // <-- prevent submitting the form
if (userId) {
dispatch(createTodo(todo, token, todoList));
}
}
Goal: Passing a function from parent to child that updates the values on parent component.
I've searched some other threads that were not using arrow functions that corrected their issue. I figure it is some sort of binding issue but I'm not sure where... This is sort of a dumbed down version of what I'm trying to do.
"react": "^18.1.0",
const Parent = () => {
const [value, setValue] = useState(0)
const update = () => {
setValue(value + 1)
}
return (
<>
{value}
<Child update={update} />
</>
)
}
I've tried passing the function a few different ways from parent to child.
<Child update={() => update()} />
<Child update={setValue} />
<Child update={() => setValue(value + 1)} />
<Child value={value} setValue={setValue} />
... and so on
const Child = ({ update }) => {
const handle = event => {
event.preventDefault()
update()
}
return (
<form onSubmit={handle}>
</form>
)
}
console.log shows update is a function in child component, and even shows the correct values to be updated - however when it is time for the function to be called I get that error.
FULL CODE
parent:
import React, { useEffect, useState, useRef } from 'react'
//Style
import { Container, Card, Button, Alert, Row, Col, Form } from 'react-bootstrap'
//Authentication
import { useAuth } from '../../authentication/AuthContext'
//Navigation
import { Link, useNavigate } from 'react-router-dom'
//Components
import Navigation from '../../components/Navigation'
import Loading from '../../components/Loading'
import CreateHOA from '../../components/CreateHOA'
import MapHOA from '../../components/MapHOA'
//Requests
import { addUser } from '../../requests/addUser'
import { getUser } from '../../requests/getUser'
const Dashboard = () => {
const [error, setError] = useState()
const [loading, setLoading] = useState(true)
const [database, setDatabase] = useState(null)
const [view, setView] = useState()
const [action, setAction] = useState({
createHoa: true
})
const { currentUser, logout } = useAuth()
const navigate = useNavigate()
const update = async () => {
getUser(currentUser.uid)
.then(res => {
console.log('get user', res)
if(res.data){
console.log('user exists')
console.log('set database')
setDatabase(res.data[0])
try{
console.log('check hoa exists')
if(res.data.hoa.length > 0){
console.log('hoa exists; set action/view')
setAction({...action, createHoa: false })
setView(res.data.hoa[0])
}
}catch(e){
console.log('hoa doesnt exist')
}
}else{
console.log('user doesnt exist')
addUser({ uid: currentUser.uid})
.then(res => {
console.log('add user', res)
console.log('set database')
setDatabase({ uid: currentUser.uid })
})
}
})
.then(() => {
console.log('set loading to false')
setLoading(false)
})
}
useEffect(() => {
update()
}, [])
return (
<>
{loading ? <Loading /> : <>
<Navigation />
<br />
<Container className='white-bg'>
<Row>
<Col xl={12}>
<h3 className='white'>Dashboard</h3>
<br /><br />
</Col>
</Row>
{action.createHoa ?
<CreateHOA uid={currentUser.uid} update={update} /> :
<>{currentUser.uid}</>
}
</Container>
<div className='footer'>
footer
</div>
</>}
</>
)
}
export default Dashboard
child
import React, { useState, useRef } from 'react'
//Style
import { Container, Card, Button, Alert, Row, Col, Form } from 'react-bootstrap'
//Components
import LoadingSmall from '../LoadingSmall'
//Requests
import { addHoa } from '../../requests/addHoa'
const CreateHOA = (uid, { update }) => {
const [loading, setLoading] = useState(false)
const nameRef = useRef()
const submit = event => {
event.preventDefault()
setLoading(true)
console.log('UID', uid)
addHoa(uid, nameRef.current.value).then(res => {
console.log(res)
update();
})
}
return (
<Row>
<Col xl={12}>
<Card>
<Card.Header>Action Needed</Card.Header>
<Card.Body>
{loading ? <LoadingSmall /> : <>
<Card.Title>Create an HOA</Card.Title>
<Card.Text>
<p>Type in the name of your Home Owners Association below and click create to get started!</p>
<Form onSubmit={submit}>
<Form.Group id='name'>
<Form.Control type='text' ref={nameRef} required />
</Form.Group>
<br />
<Button type='submit'>Create</Button>
</Form>
</Card.Text>
</>}
</Card.Body>
</Card>
</Col>
</Row>
)
}
export default CreateHOA
Use this.setValue() instead of just setValue():
const Parent = () => {
const [value, setValue] = useState(0)
const update = () => {
this.setValue(value + 1)
}
return (
<>
{value}
<Child update={update} />
</>
)
}
I have some posts in my database I'm trying to retrieve and edit posts. The posts had some categories which I set as a checkbox. Well, I've retrieved a single post by id successfully but the problem is I also retrieved the categories and I want to show them as checked not all of them only those ones which are set for that particular post. I have another problem I cannot check the box anymore and am not able to add another category to the category list. Help me!
Here is the Edit Post page
import React, { useEffect, useState } from 'react';
import { Alert, Button, Card, Container, Form } from 'react-bootstrap';
import ReactMarkdown from 'react-markdown';
import { useDispatch, useSelector } from 'react-redux';
import { toast, ToastContainer } from 'react-toastify';
import { listCategory } from '../actions/categoryActions';
import { listPostDetails, updatePost } from '../actions/postActions';
const EditPost = ({ history, match }) => {
const postId = match.params.id;
const [categories, setCategories] = useState([]);
const dispatch = useDispatch();
const categoryList = useSelector((state) => state.categoryList);
const { categories: cateList } = categoryList;
const postDetails = useSelector((state) => state.postDetails);
const { post } = postDetails;
useEffect(() => {
if (!userInfo) {
history.push('/login');
}
dispatch(listCategory());
if (!post || post._id !== postId) {
dispatch(listPostDetails(postId));
} else {
setCategories(post.categories);
}
}, [dispatch, history, userInfo, post, postId, categories]);
const submitHandler = (e) => {
e.preventDefault();
dispatch(updatePost(title, desc, img, categories));
history.push('/my_posts');
};
return (
<div className=" createPost mt-4 py-4">
<ToastContainer />
<Container>
<h2>EDIT POST</h2>
<Form onSubmit={submitHandler}>
<Form.Group controlId="category" className="mb-2">
<Form.Label>Select Categories</Form.Label>
<br />
{cateList?.map((cate) => (
<Form.Check
inline
key={cate._id}
type="checkbox"
label={cate.name}
onChange={(e) => {
if (e.target.checked) {
setCategories([...categories, cate.name]);
} else {
setCategories(
categories?.filter((cat) => cat !== cate.name)
);
}
}}
/>
))}
</Form.Group>
<Button
type="submit"
variant="success"
style={{ letterSpacing: '2px', fontWeight: 'bold' }}>
CREATE
</Button>
</Form>
</Container>
</div>
);
};
export default EditPost;
I am trying to convert this class based component into a function component. For some reason the Query (setQ) is not being read by the handleFormSubmit function. When I make the param in the setQ an object the value of the input form becomes [object, Object]. What am I doing wrong?
class component
import React, { Component } from "react";
import Jumbotron from "../components/Jumbotron";
import Card from "../components/Card";
import Form from "../components/Form";
import Book from "../components/Book";
import Footer from "../components/Footer";
import API from "../utils/API";
import { Col, Row, Container } from "../components/Grid";
import { List } from "../components/List";
class Home extends Component {
state = {
books: [],
q: "",
message: "Search For A Book To Begin!"
};
handleInputChange = event => {
const { name, value } = event.target;
this.setState({
[name]: value
});
};
getBooks = () => {
API.getBooks(this.state.q)
.then(res =>
this.setState({
books: res.data
})
)
.catch(() =>
this.setState({
books: [],
message: "No New Books Found, Try a Different Query"
})
);
};
handleFormSubmit = event => {
event.preventDefault();
this.getBooks();
};
handleBookSave = id => {
const book = this.state.books.find(book => book.id === id);
API.saveBook({
googleId: book.id,
title: book.volumeInfo.title,
subtitle: book.volumeInfo.subtitle,
link: book.volumeInfo.infoLink,
authors: book.volumeInfo.authors,
description: book.volumeInfo.description,
image: book.volumeInfo.imageLinks.thumbnail
}).then(() => this.getBooks());
};
render() {
return (
<Container>
<Row>
<Col size="md-12">
<Jumbotron>
<h1 className="text-center">
<strong>(React) Google Books Search</strong>
</h1>
<h2 className="text-center">Search for and Save Books of Interest.</h2>
</Jumbotron>
</Col>
<Col size="md-12">
<Card title="Book Search" icon="far fa-book">
<Form
handleInputChange={this.handleInputChange}
handleFormSubmit={this.handleFormSubmit}
q={this.state.q}
/>
</Card>
</Col>
</Row>
<Row>
<Col size="md-12">
<Card title="Results">
{this.state.books.length ? (
<List>
{this.state.books.map(book => (
<Book
key={book.id}
title={book.volumeInfo.title}
subtitle={book.volumeInfo.subtitle}
link={book.volumeInfo.infoLink}
authors={book.volumeInfo.authors.join(", ")}
description={book.volumeInfo.description}
image={book.volumeInfo.imageLinks.thumbnail}
Button={() => (
<button
onClick={() => this.handleBookSave(book.id)}
className="btn btn-primary ml-2"
>
Save
</button>
)}
/>
))}
</List>
) : (
<h2 className="text-center">{this.state.message}</h2>
)}
</Card>
</Col>
</Row>
<Footer />
</Container>
);
}
}
export default Home;
functional conversion
import React from "react";
// import Jumbotron from "react-bootstrap/Jumbotron";
import Row from "react-bootstrap/Row";
import Card from "../components/Card";
import Form from "../components/Form";
import Col from "react-bootstrap/Col";
import Container from "react-bootstrap/Container";
import Jumbotron from "react-bootstrap/Jumbotron";
import { useState } from "react";
import API from "../utils/API";
import Book from "../components/Book";
import Button from "react-bootstrap/Button";
import { List } from "../components/List";
import Footer from "../components/Footer";
import "./style.css";
export default function Home() {
let [books, setBooks] = useState([]);
let [q, setQ] = useState("");
let [message, setMessage] = useState("Search For A Book to Begin");
const handleInputChange = (event) => {
let { name, value } = event.target;
setQ(([name] = value));
};
let getBooks = () => {
API.getBooks(q)
.then((res) => setBooks(res.data))
.catch(() => setBooks([]));
setMessage("No New Books Found, Try a Different Query");
};
const handleFormSubmit = (event) => {
event.preventDefault();
getBooks();
};
let handleBookSave = (id) => {
const book = books.find((book) => book.id === id);
API.saveBook({
googleId: book.id,
title: book.volumeInfo.title,
subtitle: book.volumeInfo.subtitle,
link: book.volumeInfo.infoLink,
authors: book.volumeInfo.authors,
description: book.volumeInfo.description,
image: book.volumeInfo.imageLinks.thumbnail,
}).then(() => getBooks());
};
return (
<div>
<Container>
<Row>
<Col md={12}>
<Jumbotron className="rounded-3 mt-4">
<h1 className="text-center ">
<strong>(React) Google Books Search</strong>
</h1>
<h2 className="text-center">
Search for and Save Books of Interest.
</h2>
</Jumbotron>
</Col>
<Col md={12}>
<Card title="Book Search" icon=" fa-book">
<Form
handleInputChange={handleInputChange}
handleFormSubmit={handleFormSubmit}
q={q}
/>
</Card>
</Col>
</Row>
<Row>
<Col md={12}>
<Card title="Results">
{books.length ? (
<List>
{books.map((book) => (
<Book
key={book.id}
title={book.volumeInfo.title}
subtitle={book.volumeInfo.subtitle}
link={book.volumeInfo.infolink}
authors={book.volumeInfo.authors.join(", ")}
description={book.volumeInfo.description}
image={book.volumeInfo.imageLinks.thumbnail}
Btn={() => (
<Button
onClick={() => handleBookSave(book.id)}
variant="primary"
className="ml-2"
>
Save
</Button>
)}
/>
))}
</List>
) : (
<h2 className="text-center">{message}</h2>
)}
</Card>
</Col>
</Row>
<Footer />
</Container>
</div>
);
}
I'm not sure exactly what you want here, but I see the problem.
First let's look here:
state = {
books: [],
q: "",
message: "Search For A Book To Begin!"
};
handleInputChange = event => {
const { name, value } = event.target;
this.setState({
[name]: value
});
};
In a class component, state is an object with properties. You are taking advantage of that fact to allow accessing individual pieces of state with a dynamic name, and then setting that to value.
Now let's look at the functional component:
let [books, setBooks] = useState([]);
let [q, setQ] = useState("");
let [message, setMessage] = useState("Search For A Book to Begin");
const handleInputChange = (event) => {
let { name, value } = event.target;
setQ(([name] = value));
};
Note that you no longer have a single state object. You have three completely separate pieces of state. This handleInputChange function only sets one of them, q.
And [name] = value is especially strange. What that's actually doing is assigning the first character of the string in value to the variable name, which is never used or read from again. That is certainly not what you want.
In order to do what I think you want you have to call each setMyStateHere() setter explicitly.
So I think you want this:
const handleInputChange = (event) => {
let { name, value } = event.target;
if (name === 'q') {
setQ(value)
} else if (name === 'message') {
setMessage(value)
} else {
throw new Error(`Unhandled input name: ${name}`) // or something
}
};
Here you inspect name to figure out which state to set, and then call the right setter function.
Or perhaps, if you want it to closer to your class component, you can store an object in state:
let [books, setBooks] = useState([]);
let [formValues, setFormValues] = useState({
q: "",
message: "Search For A Book to Begin"
);
const handleInputChange = (event) => {
let { name, value } = event.target;
setFormValues({ ...formValues, [name]: value })
}
Now you have an object in state like you had before, and can dynamically set properties on that object.
All this assumes that you want to set something besides q from this form, otherwise this becomes trivially just:
const handleInputChange = event => { setQ(event.target.value) }
I'm trying create a search bar, when user want to search a product.
Here is my Search Input:
const [searchTerm, setSearchTerm] = useState("");
const onSubmit = (e) => {
e.preventDefault();
navigate(`/search/${searchTerm}`);
setIsShowing(false);
setOpacity(1);
};
<FormSearch onSubmit={onSubmit}>
<SearchInput type="text"
placeholder="Type something to search"
onChange={(e)=> setSearchTerm(e.target.value)}
defaultValue={searchTerm} />
<SearchButton type="submit" value="Search" />
</FormSearch>
and here is the router when click search and take user to another page:
<Router>
<SearchInfo
path="/search/:title "
searchTerm={searchTerm}
/>
</Router>
and here is my react function for the page after search:
import React, { useEffect, useState } from "react";
import styled from "styled-components";
const SearchInfo = (props) => {
const [products, setProducts] = useState([]);
const getProductsAPI = () => {
axios
.get("http://localhost:8000/api/products")
.then((res) => {
setProducts(res.data);
})
.catch((err) => {
console.log(err);
});
};
useEffect(() => {
getProductsAPI();
}, [props]);
const InfoWrapper = styled.div`
text-align: center;
`;
return (
<div>
<InfoWrapper>
{products
.filter((product) =>
product.title.includes(props.searchTerm.toUpperCase())
)
.map((filteredItem, i) => (
<Item key={i}>
<ItemTitle> {filteredItem.title} </ItemTitle>
</Item>
))}
</InfoWrapper>
</div>
);
};
export default SearchInfo;
if I refresh the page it will show all my products instead of just props.searchTerm.
How can I fix this? Seems like the props I passed from route didn't session
The searchTerm comes from the state and props you pass, not from the url. Youll need to get the param from the Router and use that instead, see https://reactrouter.com/web/api/Hooks/useparams
Something like:
<Router>
<SearchInfo path="/search/:searchterm"/>
</Router>
import { useParams } from "react-router-dom";
const SearchInfo = (props) => {
let { searchterm } = useParams();
// ...
return (
<div>
<InfoWrapper>
{products.filter((product) => product.title.includes(searchterm))
.map((filteredItem, i) => (
<Item key={i}>
<ItemTitle> {filteredItem.title} </ItemTitle>
</Item>
))}
</InfoWrapper>
</div>
);
};
I don't know why your SearchInfo have path as prop but I think path is supposed to be managed by router, so the ideal structure would be:
<Router path="/search/:searchterm" component={SearchInfo} />
Then you can easily access to location info:
const SearchInfo = (props) => {
// Here is what you need
const {
match: { params },
} = props;
}