I have a search bar on my nav component. After each keystroke, the input loses focus and you have to re-click on it to type the next key.
Here is the input:
<input
type="text"
name="search"
placeholder="Search"
value={search}
onChange={handleInputChange}
/>
Here is the handleInputChange function:
function handleInputChange(event) {
event.preventDefault();
let value = event.target.value;
setSearch(value);
}
Here is the hook for setting the search:
const [search, setSearch] = useState("");
I've tried adding a key to the input, but that doesn't work. When I move the search input to a new component, that also doesn't work.
Here is the complete code:
import React, { useEffect, useState, useCallback } from "react";
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import AppBar from '#material-ui/core/AppBar';
import Toolbar from '#material-ui/core/Toolbar';
import { Row, Col } from '../Grid';
import IconButton from '#material-ui/core/IconButton';
import SearchIcon from '#material-ui/icons/Search';
import ShoppingCartOutlinedIcon from '#material-ui/icons/ShoppingCartOutlined';
import MenuIcon from '#material-ui/icons/Menu';
import Badge from '#material-ui/core/Badge';
import useScrollTrigger from '#material-ui/core/useScrollTrigger';
import Slide from '#material-ui/core/Slide';
import SideMenu from '../SideMenu';
import { MuiThemeProvider, createMuiTheme } from '#material-ui/core/styles';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { css } from 'glamor';
import "./style.css";
function Navbar(props) {
const cart = useSelector(state => state.cart);
const [cartTotal, setCartTotal] = useState(0);
const [loggedIn, setLoggedIn] = useState(false);
const [search, setSearch] = useState("");
const [isOpen, setIsOpen] = useState(false);
const [renderMiddleCol, setMiddleCol] = useState(true);
const [menuClass, setMenuClass] = useState("no-menu");
useEffect(() => {
if (cart[0]) {
setCartTotal(cart[0].line_items.length)
}
}, [cart[0]]);
useEffect(() => {
if (window.sessionStorage.id) {
setLoggedIn(true);
} else {
setLoggedIn(false);
}
}, [loggedIn]);
useEffect(() => {
if (window.innerWidth < 450) {
setMiddleCol(false);
} else {
setMiddleCol(true);
}
}, [window.innerWidth]);
function HideOnScroll(props) {
const { children, window } = props;
const trigger = useScrollTrigger({ target: window ? window() : undefined });
return (
<Slide appear={false} direction="down" in={!trigger}>
{children}
</Slide>
);
}
HideOnScroll.propTypes = {
children: PropTypes.element.isRequired,
window: PropTypes.func,
};
function CheckCart() {
if (window.sessionStorage.id) {
window.location.href = "/cart";
} else {
toast("Please login to view your cart", {
className: css({
background: '#3E0768',
boxShadow: '2px 2px 20px 2px rgba(0,0,0,0.3)',
borderRadius: '17px'
}),
bodyClassName: css({
fontSize: '20px',
color: 'white'
}),
progressClassName: css({
background: "linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(62,7,104,1) 80%)"
})
});
}
}
function Search() {
if (search) {
sessionStorage.setItem("search", search);
window.location.href = "/search";
} else {
toast("Search field cannot be empty", {
className: css({
background: '#3E0768',
boxShadow: '2px 2px 20px 2px rgba(0,0,0,0.3)',
borderRadius: '17px'
}),
bodyClassName: css({
fontSize: '20px',
color: 'white'
}),
progressClassName: css({
background: "linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(62,7,104,1) 80%)"
})
});
}
}
function logOut(event) {
event.preventDefault();
setIsOpen(false);
sessionStorage.clear();
window.location.href = "/login";
}
function handleInputChange(event) {
event.preventDefault();
let value = event.target.value;
setSearch(value);
}
function toggleMenu(event) {
event.preventDefault();
setIsOpen(!isOpen);
if (menuClass === "no-menu") {
setMenuClass("menu-background");
} else {
setMenuClass("no-menu");
}
}
const theme = createMuiTheme({
palette: {
primary: {
main: '#000000',
contrastText: '#ffffff',
},
secondary: {
light: '#3E0768',
main: '#3E0768',
contrastText: '#ffffff',
},
tertiary: {
main: '#ffffff',
}
},
});
return (
<MuiThemeProvider theme={theme}>
<HideOnScroll {...props}>
<AppBar position="fixed" color="primary">
<Toolbar>
<Col size="md-1">
<IconButton
onClick={toggleMenu}
aria-label="Menu"
>
<MenuIcon
fontSize="large"
className="white"
/>
</IconButton>
</Col>
<Col size="md-2">
<h6>Demo Company</h6>
</Col>
{renderMiddleCol ? (
<Col size="lg-6 md-5 sm-3" />
) : (<div />)}
<Col size="md-2 4">
<Row no-gutters>
<div className="search-box">
<Col size="md-2 1">
<IconButton onClick={Search} aria-label="search" >
<SearchIcon className="white" />
</IconButton>
</Col>
<Col size="md-8 9">
{/* <SearchForm
value={search}
onChange={handleInputChange}
/> */}
<input
className="search-field white"
type="text"
name="search"
placeholder="Search"
value={search}
onChange={handleInputChange}
/>
</Col>
</div>
</Row>
</Col>
<Col size="md-1">
<IconButton
onClick={CheckCart}
aria-label="Go to cart"
>
<MuiThemeProvider theme={theme}>
<Badge
badgeContent={cartTotal}
color="secondary"
>
<ShoppingCartOutlinedIcon className="white" />
</Badge>
</MuiThemeProvider>
</IconButton>
</Col>
</Toolbar>
</AppBar>
</HideOnScroll>
<SideMenu
isOpen={isOpen}
menuClass={menuClass}
toggleMenu={toggleMenu}
loggedIn={loggedIn}
logOut={logOut}
/>
</MuiThemeProvider>
);
}
export default Navbar;
I ended up using a ref on the input and setting it focus on each re-render. Here is the code that fixed it.
const [search, setSearch] = useState("");
const searchInput = React.useRef(null);
useEffect(() => {
searchInput.current.focus();
}, [search]);
And here is the input:
<input
ref={searchInput}
className="search-field white"
type="text"
name="search"
placeholder="Search"
value={search}
onChange={handleInputChange}
/>
Credit for the solution: React: set focus on componentDidMount, how to do it with hooks?
Here's a detailed explanation that I've found helpful: https://reactkungfu.com/2015/09/react-js-loses-input-focus-on-typing/
To summarize: without an unchanging key, React is throwing away the previous instance of your controlled input upon state change and is creating a new one in its place. The input that had focus is removed immediately after its value changes.
Make sure:
Your controlled input has a key attribute
The value of key isn't derived from the input's value in any way, because you don't want the key to change when the value changes
I don't know exactly why, but my focus problem was solved by changing this code:
import { Route } from 'react-router-dom'
<Route path='xxx' component={() => <TheComponent... />}
where TheComponent contains the input element that loses focus while typing, to this code:
<Route path='xxx'>
<TheComponent... />
</Route>
See my SO question, hopefully someone will soon shed some light on how this worked
What fixed this issue for me was to not use inner components. Such as
const NewComponent = (props: {text: string}) => <div>{text}</div>;
return <div><NewComponent text="Text" /><div>;
When I declared input components this way it caused a re-render on every keystroke. The solution was to extract the components to another file or just put the whole component in the JSX without using the above method to clean up the code.
It has something to do with React not being able to know when to rerender.
Related
I have to do app using react and material ui which is combined of these components:
1- App.js
`
import './App.css';
import Header from './components/Header'
import ToDoApp from './components/ToDoApp';
function App() {
return (
<div className="App">
<Header />
<ToDoApp />
</div>
);
}
export default App;
`
2- ToDoApp.js
`
import React from 'react'
import ToDoList from './ToDoList';
import Task from './Task';
import { Container, Grid } from '#mui/material';
import {useState, } from 'react';
export default function ToDoApp() {
const [todos, setTodos] = useState([
{
id: 1,
Title: "Task 1",
done: true
},
{
id: 2,
Title: "Task 2",
done: true
},
])
const deleteTask = (id) => {
// console.log(id);
const newtodos = todos.filter(function(todo) {
return todo.id !== id;
})
setTodos(newtodos);
}
const addTodo = (todoTitle) => {
const lastTodo = todos[todos.length-1];
const newtodosId = lastTodo.id + 1;
const newtodos = todos;
newtodos.push({
id: newtodosId,
title: todoTitle,
done: false,
})
setTodos(newtodos);
console.log(newtodos);
}
return (
<Container maxWidth="sm">
<Grid item xs={12} md={6}>
<ToDoList newTodo={addTodo}/>
<Task ToDoAppList={todos} DeleteTodoTask={deleteTask}/>
</Grid>
</Container>
)
}
`
3- ToDoList.js
`
import * as React from 'react';
import FormControl from '#mui/material/FormControl';
import Typography from '#mui/material/Typography';
import TextField from '#mui/material/TextField';
import IconButton from '#mui/material/IconButton';
import { Add, AddToDriveOutlined } from '#mui/icons-material/';
import { useState } from 'react';
import { useEffect } from 'react';
export default function ToDoList(props) {
const addTodo = props.newTodo
const [todo, setTodo] = useState('')
function writeTodo(e) {
// console.log(e.target.value);
setTodo(e.target.value);
}
const addText = (e) => {
e.preventDefault();
addTodo(todo);
// console.log(todo);
}
return (
<>
<Typography sx={{ mt: 4, mb: 2 }} variant="h6" component="div">
ToDo List
</Typography>
<form onSubmit={(e) => addText(e)}>
<FormControl>
<div
style={{display: 'flex', marginBottom: 20}}>
<TextField
id="standard-helperText"
label="New ToDo"
style={{ width: 450 }}
variant="standard"
value={todo}
onChange={(e) => writeTodo(e)}
/>
<IconButton edge="end" aria-label="create" type="submit">
<Add />
</IconButton>
</div>
</FormControl>
</form>
</>
)
}
`
4-Task.js
`
import React from 'react'
import Paper from '#mui/material/Paper';
import IconButton from '#mui/material/IconButton';
import { Tag, Check, Delete } from '#mui/icons-material';
export default function Task(props) {
const tasks = props.ToDoAppList;
const deleteTask = props.DeleteTodoTask;
const List = tasks.map(task => { return(
<Paper elevation={3} style={{padding: 10, marginTop: 10}} key={task.id}>
<IconButton aria-label="create">
<Tag />
</IconButton>
<span style={{textDecoration: 'line-through'}}>{task.Title}</span>
<IconButton aria-label="delete" style={{float: 'right', color: 'red'}} onClick={()=>deleteTask(task.id)}>
<Delete />
</IconButton>
<IconButton aria-label="check" style={{float: 'right'}}>
<Check />
</IconButton>
</Paper>
)})
return (
<>
{List}
</>
)
}
`
When Submitting the new Todo, the result is not shown on the DOM.
In the ToDoApp.js -> function "addTodo" I used setState to push the new array with the new item, and is sent to ToDoList.js -> form "onSubmit" event in "addText" function. I put the console.log in the function and I am getting the new array but it's not showing on the page.
The tasks listing is handled in the task.js by sending the array through props and using map function to loop over the array.
Note: I handled the delete function in the ToDoApp.js and it's woking with no problem. I don't understand why it's not running with "addTodo" funciton.
First you need to change title to Title (based on your state Schema)
then you dont need to update whole state just add your new task to your current state (function in setTodos)
const addTodo = (todoTitle) => {
const lastTodo = todos[todos.length - 1];
const newtodosId = lastTodo.id + 1;
let newTodo = {
id: newtodosId,
Title: todoTitle,
done: false
};
setTodos((prev)=>[...prev,newTodo])
};
I am new to react and self-taught, struggling with state and react-select
I have a dropdown with react-select. Depending on what value the user selects I want to display the relevant data onto the screen. The data is coming from the authContext(useContext). This is what I have written so far. But its not working. Can someone please guide me in the right direction:
import React, { useContext, useState } from 'react'
import styles from './FullRecord.module.css'
import {AuthContext} from '../../shared/context/auth-context'
import Select from 'react-select'
import { makeStyles } from '#material-ui/core/styles';
import Card from '#material-ui/core/Card';
import CardContent from '#material-ui/core/CardContent';
import Typography from '#material-ui/core/Typography';
const useStyles = makeStyles({
custom: {
backgroundColor: "#558d6b",
fontWeight: "bold"
},
customFont: {
fontWeight: "bold",
fontSize: "20px"
},
customFont1: {
fontWeight: "light"
}
});
const FullRecord = (props) => {
const auth = useContext(AuthContext)
const classes = useStyles();
const [selectedValue, setSelectedValue] = useState('');
const [tableValue, setTableValue] = useState(false)
let data
const options = auth.tournaments.map((tournament) => {
return {
value: tournament.TournamentName,
label: tournament.TournamentName,
}
})
const handleChange = (selectedValue) => {
setSelectedValue(selectedValue);
setTableValue(true)
const {value} = selectedValue
let tname
if(value === 'GSM Edition 1'){
const noOfMatches = auth.profile.MemberMatches.filter((match) => match.TournamentName === 'GSM Edition 1')
if(tableValue){
return (
<div>
<li className={styles['member-item']}>
<Card className={classes.custom} variant="outlined">
<CardContent>
<Typography className={classes.customFont} gutterBottom>
Number Of Matches Played
</Typography>
<Typography className={classes.customFont}>
{noOfMatches}
</Typography>
</CardContent>
</Card>
</li>
</div>
)
}
}
}
return (
<React.Fragment>
<div className={styles['fullrecord__maindiv']}>
<Select
onChange={handleChange}
options={options}
/>
</div>
</React.Fragment>
)
}
export default FullRecord
I would say that your first problem stems from using a function argument name that is the same as your state variable. Especially a problem as the function is also an arrow function.
const handleChange = (newValue) => {
setSelectedValue(newValue);
setTableValue(true);
}
You're also trying to return JSX from your event handler, where it seems like what you really want is the core content to change when your selection changes. Ultimately, that logic goes in to your FullRecord return statement, and will automatically update as state is updated.
const FullRecord = (props) => {
// ...bunch of stuff, then
return (
<React.Fragment> {/* really don't need here, as you only have one root element */}
<div className={styles['fullrecord__maindiv']}>
<Select
onChange={handleChange}
options={options}
/>
{selectedValue ? (
{/* output something here */}
) : null}
</div>
</React.Fragment>
)
}
So I have a basic messenger chap application, the search bar to retrieve chats works fine whenever I type in letters, yet whenever I backspace, the event handler wont update, and I am left with the chat room lists that fit the search, even though the search bar is left empty.
Examples below
1.All chatrooms are shown below:
2.Typing "funny" filters a list with chatrooms labeled "funny":
3.Pressing backspace does not show all the chatrooms:
import { Avatar, IconButton } from '#material-ui/core';
import React, { useEffect, useState } from 'react';
import './Sidebar.css';
import SearchIcon from '#material-ui/icons/Search';
import { RateReviewOutlined } from '#material-ui/icons';
import { SidebarChat } from './SidebarChat';
import { useSelector } from 'react-redux';
import { selectUser } from './features/userSlice';
import db, { auth } from './firebase';
import { makeStyles } from '#material-ui/core/styles';
import Modal from '#material-ui/core/Modal';
import Backdrop from '#material-ui/core/Backdrop';
import Fade from '#material-ui/core/Fade';
const useStyles = makeStyles((theme) => ({
modal: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
paper: {
backgroundColor: theme.palette.background.paper,
border: '2px solid #000',
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 3),
},
}));
export function Sidebar(props) {
const user = useSelector(selectUser);
const [chats, setChats] = useState([]);
const classes = useStyles();
const [open, setOpen] = useState(false);
const [search, setSearch] = useState('');
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
useEffect(() => {
db.collection('chats').onSnapshot((snapshot) =>
setChats(
snapshot.docs.map((doc) => ({
id: doc.id,
data: doc.data(),
}))
)
);
}, []);
const addChat = () => {
const chatName = prompt('Please enter a chat name');
if (chatName) {
db.collection('chats').add({
chatName: chatName,
});
}
};
const searchFunction = (e) => {
setSearch(e);
console.log(search);
console.log(chats);
const filtered = chats.filter(chat => {
return chat.data.chatName.toLowerCase().includes(e.toLowerCase())
});
// console.log(filtered)
setChats(filtered);
};
return (
<div className="sidebar">
<div className="sidebar__header">
<Avatar
onClick={handleOpen}
src={user.photo}
className="sidebar__avatar"
/>
<div className="sidebar__input">
<SearchIcon />
<input
placeholder="Search"
value={search}
onChange={(e) => searchFunction(e.target.value)}
/>
</div>
<IconButton variant="outlined" className="sidebar__inputButton">
<RateReviewOutlined onClick={addChat} />
</IconButton>
</div>
<div className="sidebar__chats">
{chats.map(({ id, data: { chatName } }) => (
<SidebarChat key={id} id={id} chatName={chatName} />
))}
</div>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
className={classes.modal}
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Fade in={open}>
<div className={classes.paper}>
<h2 id="transition-modal-title">
Are you sure you want to sign out?
</h2>
<button onClick={() => auth.signOut()}>Yes</button>
<button onClick={handleClose}>No</button>
</div>
</Fade>
</Modal>
</div>
);
}
In your searchFunction it is constantly updating filtered to have less and less chatrooms. You could store chat rooms as a separate state and then filter that instead to preserve all chat rooms.
So I have a basic messenger chat application with a search bar to retrieve chatrooms, and it works fine whenever I type in letters, yet whenever I backspace, why wont it retrieve the list I had previous?
Examples below
1.All chatrooms are shown below:
2.Typing "funny" filters a list with chatrooms labeled "funny":
3.Pressing backspace does not show all the chatrooms:
import { Avatar, IconButton } from '#material-ui/core';
import React, { useEffect, useState } from 'react';
import './Sidebar.css';
import SearchIcon from '#material-ui/icons/Search';
import { RateReviewOutlined } from '#material-ui/icons';
import { SidebarChat } from './SidebarChat';
import { useSelector } from 'react-redux';
import { selectUser } from './features/userSlice';
import db, { auth } from './firebase';
import { makeStyles } from '#material-ui/core/styles';
import Modal from '#material-ui/core/Modal';
import Backdrop from '#material-ui/core/Backdrop';
import Fade from '#material-ui/core/Fade';
const useStyles = makeStyles((theme) => ({
modal: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
paper: {
backgroundColor: theme.palette.background.paper,
border: '2px solid #000',
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 3),
},
}));
export function Sidebar(props) {
const user = useSelector(selectUser);
const [chats, setChats] = useState([]);
const classes = useStyles();
const [open, setOpen] = useState(false);
const [search, setSearch] = useState('');
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
useEffect(() => {
db.collection('chats').onSnapshot((snapshot) =>
setChats(
snapshot.docs.map((doc) => ({
id: doc.id,
data: doc.data(),
}))
)
);
}, []);
const addChat = () => {
const chatName = prompt('Please enter a chat name');
if (chatName) {
db.collection('chats').add({
chatName: chatName,
});
}
};
const searchFunction = (e) => {
setSearch(e);
console.log(search);
console.log(chats);
const filtered = chats.filter(chat => {
return chat.data.chatName.toLowerCase().includes(e.toLowerCase())
});
// console.log(filtered)
setChats(filtered);
};
return (
<div className="sidebar">
<div className="sidebar__header">
<Avatar
onClick={handleOpen}
src={user.photo}
className="sidebar__avatar"
/>
<div className="sidebar__input">
<SearchIcon />
<input
placeholder="Search"
value={search}
onChange={(e) => searchFunction(e.target.value)}
/>
</div>
<IconButton variant="outlined" className="sidebar__inputButton">
<RateReviewOutlined onClick={addChat} />
</IconButton>
</div>
<div className="sidebar__chats">
{chats.map(({ id, data: { chatName } }) => (
<SidebarChat key={id} id={id} chatName={chatName} />
))}
</div>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
className={classes.modal}
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Fade in={open}>
<div className={classes.paper}>
<h2 id="transition-modal-title">
Are you sure you want to sign out?
</h2>
<button onClick={() => auth.signOut()}>Yes</button>
<button onClick={handleClose}>No</button>
</div>
</Fade>
</Modal>
</div>
);
}
You need to save the initial list of chats in a variable and once the search is set to blank '' you need to update your chats with the initial saved chats.
Problem here is -
const filtered = chats.filter(chat => {
return chat.data.chatName.toLowerCase().includes(e.toLowerCase())
});
// console.log(filtered)
setChats(filtered);
I am new to React and MUI and maybe I am just missing something.
I am trying to make a button with color='warning' that is defined in my palette like this (the theme works and I can use primary and secondary colors):
const theme = createMuiTheme({
palette: {
primary: {
main: '#70B657'
},
secondary: {
light: '#2face3',
main: '#4199D8',
contrastText: '#ffcc00'
},
warning: {
main: '#BC211D'
}
}
});
I noticed in the documentation that the <Button> color prop only takes default|inherit|primary|secondary so it is not possible to use it like that.
So what is the CORRECT or best practice to use warning colored button in Material-UI? I think this is a basic thing and should be pretty easy to achieve..??
Preferably a solution that does not involve making several different Themes and importing them when needed.
Thanks!
Usage:
const useStyles = makeStyles(theme => ({
root: {
color: theme.palette.warning.main
}
}));
Full code:
import React from "react";
import "./styles.css";
import { Button } from "#material-ui/core";
import { createMuiTheme, ThemeProvider } from "#material-ui/core/styles";
import { makeStyles } from "#material-ui/core/styles";
const useStyles = makeStyles(theme => ({
root: {
color: theme.palette.warning.main
}
}));
function YourComponent() {
const classes = useStyles();
return (
<div className="App">
<Button variant="contained" classes={{ root: classes.root }}>
Secondary
</Button>
</div>
);
}
const theme = createMuiTheme({
palette: {
warning: { main: "#FFFFFF" }
}
});
export default function App() {
return (
<ThemeProvider theme={theme}>
<YourComponent />
</ThemeProvider>
);
}
Update
Pass props to makeStyles
import React from "react";
import "./styles.css";
import { makeStyles } from "#material-ui/core/styles";
const useStyles = props =>
makeStyles(theme => ({
root: {
color: props.value === "111" ? "red" : "blue"
}
}));
const Comp = props => {
const classes = useStyles(props)();
return <input defaultValue={props.value} className={classes.root} />;
};
export default function App() {
return (
<div className="App">
<div>
<Comp value={"111"} />
</div>
<div>
<Comp value={"222"} />
</div>
</div>
);
}
yeah I don't understand why the first example would work and the second dont.
app component
const theme = createMuiTheme({
palette: {
primary: {
main: '#bed000',
},
secondary: {
main: '#110b36',
},
error: {
main: '#B33A3A',
},
},
})
<MuiThemeProvider theme={theme}>
<Route exact path="/" component={LoginView} />
</MuiThemeProvider>
<LoginView>
<TextField
autoFocus
label="ContraseƱa"
name="Password"
type="Password"
value={values.Password}
onChange={handleChange}
onBlur={handleBlur}
fullWidth
color={touched.Password && errors.Password ? "primary" : "secondary"}
/>
<TextField
autoFocus
label="ContraseƱa"
name="Password"
type="Password"
value={values.Password}
onChange={handleChange}
onBlur={handleBlur}
fullWidth
color={touched.Password && errors.Password ? "error" : "secondary"}
/>
</LoginView>