conditional slider with material-ui - reactjs

I am trying to construct a single Slider component in React that will enable me to choose whether I want a slider with two values or one, and store them in the component state. The slider with two values works fine, but the slider with only a single value does not move, however, I can console.log() the correct values. What can I do to fix it?
import React from "react";
import Slider from "#material-ui/core/Slider";
export default function GameOptions() {
return (
<div className="screen">
<GameSlider values={1} max={50} />
<GameSlider values={2} max={50} />
</div>
);
}
function GameSlider(props) {
const [value, setValue] = React.useState([0, props.max]);
const handleChange = (event, newValue) => {
setValue(newValue);
console.log(newValue);
};
const getValue = () => {
if (props.values == 1) {
return value[1];
}
return value;
};
const getSliderType = () => {
if (props.values == 1) {
return "range-slider";
}
return "continuous-slider";
};
return (
<Slider
value={getValue()}
onChange={handleChange}
valueLabelDisplay="auto"
aria-labelledby={getSliderType()}
/>
);
}

When "continuous-slider" is in use, the value received by the handleChange function will be a single numeric value, as opposed to a numeric array. So your handleChange function should treat it as that.
const handleChange = (event, newValue) => {
setValue((props.values == 1) ? [newValue, newValue] : newValue);
console.log(newValue);
};
Additionally I think your getSliderType function should be the otherway.
const getSliderType = () => {
return (props.values == 1) ? "continuous-slider" : "range-slider";
};

Related

How to save edited text on refresh?

I'm using a React component called "EdiText". Right now, I can save the text, but if I refresh the page it goes back to its initial value. I want to save and keep the edited value even after a page refresh.
My code is the following:
const Project = ({ project, onDelete}) => {
const [value, setValue] = useState('')
const handleSave = (val) => {
console.log('Edited value ->', val);
setValue(val);
}
return (
<div>
<div className="project-component">
<EdiText
type="text"
className="project-title"
value={project.title}
onSave={handleSave}
showButtonsOnHover
submitOnUnfocus
cancelOnUnfocus
/>
<br/>
<EdiText
type="textarea"
className="project-description"
value={project.description}
onSave={handleSave}
showButtonsOnHover
submitOnUnfocus
cancelOnUnfocus
/>
<Link to={`/projectpage/${project.id}`} className="view-icon"><FaEye/></Link>
<FaTrash className="delete-icon" onClick={() => onDelete(project.id)}/>
</div>
</div>
)
}
You can use a custom hook for this.
// Hook
function useLocalStorage(key, initialValue) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};
return [storedValue, setValue];
}
Store it onto another file and import and use like this:
const [value, setValue] = useLocalStorage('')
You can use localStorage:
const handleSave = (val) => {
console.log('Edited value ->', val);
localStorage.setItem('value', val)
setValue(val);
}
And when the component first renders:
useEffect(() => {
const storedVal = localStorage.getItem('value')
if(storedVal) setValue(val)
}, [])
You could use localStorage:
const Project = ({ project, onDelete}) => {
const [value, setValue] = useState(localStorage.getItem('value'))
const handleSave = (val) => {
console.log('Edited value ->', val);
setValue(val);
localStorage.setItem('value', val)
}
return (
<div>
<div className="project-component">
<EdiText
type="text"
className="project-title"
value={project.title}
onSave={handleSave}
showButtonsOnHover
submitOnUnfocus
cancelOnUnfocus
/>
<br/>
<EdiText
type="textarea"
className="project-description"
value={project.description}
onSave={handleSave}
showButtonsOnHover
submitOnUnfocus
cancelOnUnfocus
/>
<Link to={`/projectpage/${project.id}`} className="view-icon"><FaEye/></Link>
<FaTrash className="delete-icon" onClick={() => onDelete(project.id)}/>
</div>
</div>
)
}

Using React hook form getValues() within useEffect return function, returns {}

I'm using react-hook-form library with a multi-step-form
I tried getValues() in useEffect to update a state while changing tab ( without submit ) and it returned {}
useEffect(() => {
return () => {
const values = getValues();
setCount(values.count);
};
}, []);
It worked in next js dev, but returns {} in production
codesandbox Link : https://codesandbox.io/s/quirky-colden-tc5ft?file=/src/App.js
Details:
The form requirement is to switch between tabs and change different parameters
and finally display results in a results tab. user can toggle between any tab and check back result tab anytime.
Implementation Example :
I used context provider and custom hook to wrap setting data state.
const SomeContext = createContext();
const useSome = () => {
return useContext(SomeContext);
};
const SomeProvider = ({ children }) => {
const [count, setCount] = useState(0);
const values = {
setCount,
count
};
return <SomeContext.Provider value={values}>{children}</SomeContext.Provider>;
};
Wrote form component like this ( each tab is a form ) and wrote the logic to update state upon componentWillUnmount.
as i found it working in next dev, i deployed it
const FormComponent = () => {
const { count, setCount } = useSome();
const { register, getValues } = useForm({
defaultValues: { count }
});
useEffect(() => {
return () => {
const values = getValues(); // returns {} in production
setCount(values.count);
};
}, []);
return (
<form>
<input type="number" name={count} ref={register} />
</form>
);
};
const DisplayComponent = () => {
const { count } = useSome();
return <div>{count}</div>;
};
Finally a tab switching component & tab switch logic within ( simplified below )
const App = () => {
const [edit, setEdit] = useState(true);
return (
<SomeProvider>
<div
onClick={() => {
setEdit(!edit);
}}
>
Click to {edit ? "Display" : "Edit"}
</div>
{edit ? <FormComponent /> : <DisplayComponent />}
</SomeProvider>
);
}

React useRef with Array for dynamic

I am trying to make the component that the focus moves to the next input when each letter inputted.
I think I need multiple ref like an array but I don't know about it.
It's a sample code for the question.
function PIN({length, onChange, value}){
const inputEl = React.useRef(null);
function handleChange(e){
onChange(e);
inputEl.current.focus();
}
return (
<div>
{
new Array(length).fill(0).map((i)=>(
<input type="text" ref={inputEl} onChange={handleChange} />
))
}
</div>
)
}
You can create multiple refs
function PIN({length, onChange, value}){
const inputRefs = useMemo(() => Array(length).fill(0).map(i=> React.createRef()), []);
const handleChange = index => (e) => {
//onChange(e); // don't know about the logic of this onChange if you have multiple inputs
if (inputRefs[index + 1]) inputRefs[index + 1].current.focus();
}
return (
<div>
{
new Array(length).fill(0).map((inp, index)=>(
<input type="text" ref={inputRefs[index]} onChange={handleChange(index)} />
))
}
</div>
)
}
The ref on input is equivalent to a callback function. You can pass a method to him. The parameter received by this method is the input dom element, which you can store in an array.
import React from "react";
import "./styles.css";
export default function App() {
const inputEl = React.useRef([]);
function handleChange(i){
inputEl.current[i+1].focus();
}
return (
<div>
{
new Array(3).fill(0).map((n,i)=>(
<input
key={i}
type="text"
ref={ref=>inputEl.current.push(ref)}
onChange={()=>handleChange(i)}
/>
))
}
</div>
)
}
In your inputs, you can pass a function to the ref parameter, this will allow you to store all of your refs in an array:
let myRefs = [];
const saveThisRef = (element) => {
myRefs.push(element);
}
Then you can pass your function to each input you render:
<input type="text" ref={saveThisRef} onChange={handleChange} />
Then you can advance to the next input in the onChange handler:
// Find the index of the next element
const index = myRefs.indexOf(element) + 1;
// Focus it
if (index < myRefs.length) myRefs[index].focus();
Re-rendering the component that holds the dynamic Refs list with a different number of refs raises an exception ("Rendered more hooks than during the previous render"), as you can see in this example:
https://codesandbox.io/s/intelligent-shannon-u3yo6?file=/src/App.js
You can create a new component that renders a single and holds it's own single ref, and use the parent element to manage the current focused input, and pass this data to you'r new component, for example.
Here is an example that would actually work:
const { useState, useCallback, useEffect, useRef } = React;
const Pin = ({ length, onChange, value }) => {
const [val, setVal] = useState(value.split(''));
const [index, setIndex] = useState(0);
const arr = [...new Array(length)].map(
(_, index) => index
);
const myRefs = useRef(arr);
const saveThisRef = (index) => (element) => {
myRefs.current[index] = element;
};
function handleChange(e) {
const newVal = [...val];
newVal[index] = e.target.value;
if (index < length - 1) {
setIndex(index + 1);
}
setVal(newVal);
onChange(newVal.join(''));
}
const onFocus = (index) => () => {
const newVal = [...val];
newVal[index] = '';
setIndex(index);
setVal(newVal);
onChange(newVal.join(''));
};
useEffect(() => {
if (index < myRefs.current.length) {
myRefs.current[index].focus();
}
}, [index, length, myRefs]);
return arr.map((index) => (
<input
type="text"
ref={saveThisRef(index)}
onChange={handleChange}
onFocus={onFocus(index)}
value={val[index] || ''}
maxLength="1"
key={index}
/>
));
};
const App = () => {
const [value, setValue] = useState('');
const onChange = useCallback(
(value) => setValue(value),
[]
);
console.log('value:', value);
return (
<Pin
length={5}
value={value}
onChange={onChange}
/>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
All answers will shift focus to next input when you correct an already set value. The requirement is that focus should shift when a letter is inputted, not when you remove a value.

How to optimize React components with React.memo and useCallback when callbacks are changing state in the parent

I've come accross a performance optimization issue that I feel could be fixed somehow but I'm not sure how.
Suppose I have a collection of objects that I want to be editable. The parent component contains all objects and renders a list with an editor component that shows the value and also allows to modify the objects.
A simplified example would be this :
import React, { useState } from 'react'
const Input = props => {
const { value, onChange } = props
handleChange = e => {
onChange && onChange(e.target.value)
}
return (
<input value={value} onChange={handleChange} />
)
}
const ObjectEditor = props => {
const { object, onChange } = props
return (
<li>
<Input value={object.name} onChange={onChange('name')} />
</li>
)
}
const Objects = props => {
const { initialObjects } = props
const [objects, setObjects] = useState(initialObjects)
const handleObjectChange = id => key => value => {
const newObjects = objects.map(obj => {
if (obj.id === id) {
return {
...obj,
[key]: value
}
}
return obj
})
setObjects(newObjects)
}
return (
<ul>
{
objects.map(obj => (
<ObjectEditor key={obj.id} object={obj} onChange={handleObjectChange(obj.id)} />
))
}
</ul>
)
}
export default Objects
So I could use React.memo so that when I edit the name of one object the others don't rerender. However, because of the onChange handler being recreated everytime in the parent component of ObjectEditor, all objects always render anyways.
I can't solve it by using useCallback on my handler since I would have to pass it my objects as a dependency, which is itself recreated everytime an object's name changes.
It seems to me like it is not necessary for all the objects that haven't changed to rerender anyway because the handler changed. And there should be a way to improve this.
Any ideas ?
I've seen in the React Sortly repo that they use debounce in combination with each object editor changing it's own state.
This allows only the edited component to change and rerender while someone is typing and updates the parent only once if no other change event comes up in a given delay.
handleChangeName = (e) => {
this.setState({ name: e.target.value }, () => this.change());
}
change = debounce(() => {
const { index, onChange } = this.props;
const { name } = this.state;
onChange(index, { name });
}, 300);
This is the best solution I can see right now but since they use the setState callback function I haven't been able to figure out a way to make this work with hooks.
You have to use the functional form of setState:
setState((prevState) => {
// ACCESS prevState
return someNewState;
});
You'll be able to access the current state value (prevState) while updating it.
Then way you can use the useCallback hook without the need of adding your state object to the dependency array. The setState function doesn't need to be in the dependency array, because it won't change accross renders.
Thus, you'll be able to use React.memo on the children, and only the ones that receive different props (shallow compare) will re-render.
EXAMPLE IN SNIPPET BELOW
const InputField = React.memo((props) => {
console.log('Rendering InputField '+ props.index + '...');
return(
<div>
<input
type='text'
value={props.value}
onChange={()=>
props.handleChange(event.target.value,props.index)
}
/>
</div>
);
});
function App() {
console.log('Rendering App...');
const [inputValues,setInputValues] = React.useState(
['0','1','2']
);
const handleChange = React.useCallback((newValue,index)=>{
setInputValues((prevState)=>{
const aux = Array.from(prevState);
aux[index] = newValue;
return aux;
});
},[]);
const inputItems = inputValues.map((item,index) =>
<InputField
value={item}
index={index}
handleChange={handleChange}
/>
);
return(
<div>
{inputItems}
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
Okay, so it seems that debounce works if it's wrapped in useCallback
Not sure why it doesn't seem to be necessary to pass newObject as a dependency in the updateParent function though.
So to make this work I had to make the following changes :
First, useCallback in the parent and change it to take the whole object instead of being responsible for updating the keys.
Then update the ObjectEditor to have its own state and handle the change to the keys.
And wrap the onChange handler that will update the parent in the debounce
import React, { useState, useEffect } from 'react'
import debounce from 'lodash.debounce'
const Input = props => {
const { value, onChange } = props
handleChange = e => {
onChange && onChange(e.target.value)
}
return (
<input value={value} onChange={handleChange} />
)
}
const ObjectEditor = React.memo(props => {
const { initialObject, onChange } = props
const [object, setObject] = useState(initialObject)
const updateParent = useCallback(debounce((newObject) => {
onChange(newObject)
}, 500), [onChange])
// synchronize the object if it's changed in the parent
useEffect(() => {
setObject(initialObject)
}, [initialObject])
const handleChange = key => value => {
const newObject = {
...object,
[key]: value
}
setObject(newObject)
updateParent(newObject)
}
return (
<li>
<Input value={object.name} onChange={handleChange('name')} />
</li>
)
})
const Objects = props => {
const { initialObjects } = props
const [objects, setObjects] = useState(initialObjects)
const handleObjectChange = useCallback(newObj => {
const newObjects = objects.map(obj => {
if (newObj.id === id) {
return newObj
}
return obj
})
setObjects(newObjects)
}, [objects])
return (
<ul>
{
objects.map(obj => (
<ObjectEditor key={obj.id} initialObject={obj} onChange={handleObjectChange} />
))
}
</ul>
)
}
export default Objects

react hooks setTimeout after setState

I recently wanted to design an input component with react hooks.
The component would check validation after entering input in 0.5 second.
my code like
const inputField = ({ name, type, hint, inputValue, setInput }) => {
// if there is default value, set default value to state
const [value, setValue] = useState(inputValue);
// all of validation are true for testing
const validCheck = () => true;
let timeout;
const handleChange = e => {
clearTimeout(timeout);
const v = e.target.value;
setValue(v);
timeout = setTimeout(() => {
// if valid
if (validCheck()) {
// do something...
}
}, 500);
};
return (
<SCinputField>
<input type={type} onChange={handleChange} />
</SCinputField>
);
};
unfortunately, it's not worked, because the timeout variable would renew every time after setValue.
I found react-hooks provide some feature like useRef to store variable.
Should I use it or shouldn't use react-hooks in this case?
Update
add useEffect
const inputField = ({ name, type, hint, inputValue, setInput }) => {
// if there is default value, set default value to state
const [value, setValue] = useState(inputValue);
// all of validation are true for testing
const validCheck = () => true;
let timeout;
const handleChange = e => {
const v = e.target.value;
setValue(v);
};
// handle timeout
useEffect(() => {
let timeout;
if (inputValue !== value) {
timeout = setTimeout(() => {
const valid = validCheck(value);
console.log('fire after a moment');
setInput({
key: name,
valid,
value
});
}, 1000);
}
return () => {
clearTimeout(timeout);
};
});
return (
<SCinputField>
<input type={type} onChange={handleChange} />
</SCinputField>
);
};
It looks worked, but I am not sure about it's a right way to use.
Here's how I would do it:
import React, {useState, useEffect, useRef} from 'react';
function InputField() {
const [value, setValue] = useState(''); // STATE FOR THE INPUT VALUE
const timeoutRef = useRef(null); // REF TO KEEP TRACK OF THE TIMEOUT
function validate() { // VALIDATE FUNCTION
console.log('Validating after 500ms...');
}
useEffect(() => { // EFFECT TO RUN AFTER CHANGE IN VALUE
if (timeoutRef.current !== null) { // IF THERE'S A RUNNING TIMEOUT
clearTimeout(timeoutRef.current); // THEN, CANCEL IT
}
timeoutRef.current = setTimeout(()=> { // SET A TIMEOUT
timeoutRef.current = null; // RESET REF TO NULL WHEN IT RUNS
value !== '' ? validate() : null; // VALIDATE ANY NON-EMPTY VALUE
},500); // AFTER 500ms
},[value]); // RUN EFFECT AFTER CHANGE IN VALUE
return( // SIMPLE TEXT INPUT
<input type='text'
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
WORKING EXAMPLE ON SNIPPET BELOW:
function InputField() {
const [value, setValue] = React.useState('');
const timeoutRef = React.useRef(null);
function validate() {
console.log('Validating after 500ms...');
}
React.useEffect(() => {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(()=> {
timeoutRef.current = null;
value !== '' ? validate() : null;
},500);
},[value]);
return(
<input type='text' value={value} onChange={(e) => setValue(e.target.value)}/>
);
}
ReactDOM.render(<InputField/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
You don't need to keep the reference to the timeout between renders. You can just return a function from the useEffect to clear it:
React.useEffect(() => {
const timeout = setTimeout(()=> {
if (value !== '') {
validate();
}
}, 500);
return () => {
clearTimeout(timeout); // this guarantees to run right before the next effect
}
},[value, validate]);
Also, don't forget to pass all the dependencies to the effect, including the validate function.
Ideally, you would pass the value as a parameter to the validate function: validate(value) - this way, the function has fewer dependencies, and could even be pure and moved outside the component.
Alternatively, if you have internal dependencies (like another setState or an onError callback from props), create the validate function with a useCallback() hook :
const validate = useCallback((value) => {
// do something with the `value` state
if ( /* value is NOT valid */ ) {
onError(); // call the props for an error
} else {
onValid();
}
}, [onError, onValid]); // and any other dependencies your function may use
This will keep the same function reference between the renders if the dependencies don't change.
You can move timeout variable inside handleChange method.
const inputField = ({ name, type, hint, inputValue, setInput }) => {
// if there is default value, set default value to state
const [value, setValue] = useState(inputValue);
// all of validation are true for testing
const validCheck = () => true;
const handleChange = e => {
let timeout;
clearTimeout(timeout);
const v = e.target.value;
setValue(v);
timeout = setTimeout(() => {
// if valid
if (validCheck()) {
// do something...
}
}, 500);
};
return (
<SCinputField>
<input type={type} onChange={handleChange} />
</SCinputField>
);
};

Resources