I have table with multiple columns. Each column can have modal for filtering.
I want only one modal to be visible at a time.
I pass state of component and setter of state to child component.
I can open it on click on button and I want to be able to close it by clicking same button.
const Parent = () => {
const [search, setSearch] = React.useState(null)
return
{columns.map(x => {
<...>
<Child showSearch={search} setSearch={setSearch} column={x} />
</...>
})}
}
const Child = ({ showSearch, setSearch, column }) => {
const isCurrentShowed = showSearch === column.id
const escFunction = useCallback((event) => {
if (event.keyCode === 27) {
setSearch(null);
}
}, [setSearch]);
const toggleSearch = () => {
console.log("Search variable is always null)
setSearch(showSearch === column.id ? null : column.id)
}
useEffect(() => {
document.addEventListener('keydown', escFunction, false);
return () => {
document.removeEventListener('keydown', escFunction, false);
};
}, [escFunction]);
useEffect(() => {
if (isCurrentShowed && inputRef && inputRef.current) {
inputRef.current.focus();
}
}, [isCurrentShowed]);
return (
<div>
<button onClick={toggleSearch}>
button
</button>
{isCurrentShowed && (
<div>
{column.render('Filter', { inputRef: inputRef})}
</div>
)}
</div>
);
}
In child component variable showSearch is available and I see changed value.
In toggleSearch funtion variable showSearch is always null and I'm not sure why.
You are losing this reference
Swap this line:
<button onClick={toggleSearch}>
With this:
<button onClick={toggleSearch.bind(this)}>
More about context: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this
I think column value is not properly passed to child.
const Parent = () => {
const [search, setSearch] = React.useState(null)
return
{columns.map(x => {
<...>
<Child showSearch={search} setSearch={setSearch} column={x} />
</...>
})}
}
setSearch(showSearch === column.id ? null : column.id)
Care this part. Are you sure of that column.id is not null?
If it is null then always provides the equality of null === null so ternary returns null again.
Related
[Solved] My input component is losing focus as soon as I press any key only when its value is controlled from outside the portal
NOTE: I am sorry. While writing this, I found the problem in my code, but I decided to post this anyway
[Reason] I was inlining the close function, so the useEffect hook got triggered every time close changed when the component was rendered again due to state changes and thus calling the activeElement.blur() on each keystroke.
Portal
const root = document.getElementById('root')
const modalRoot = document.getElementById('modal-root')
const Portal = ({ children, className, drawer = false }) => {
const element = React.useMemo(() => document.createElement('div'), [])
React.useEffect(() => {
element.className = clsx('modal', className)
modalRoot.appendChild(element)
return () => {
modalRoot.removeChild(element)
}
}, [element, className])
return ReactDOM.createPortal(children, element)
}
Modal
const Modal = (props) => {
const { children, show = false, close, className } = props
const backdrop = React.useRef(null)
const handleTransitionEnd = React.useCallback(() => setActive(show), [show])
const handleBackdropClick = React.useCallback(
({ target }) => target === backdrop.current && close(),
[]
)
const handleKeyUp = React.useCallback(
({ key }) => ['Escape'].includes(key) && close(),
[]
)
React.useEffect(() => {
if (backdrop.current) {
window.addEventListener('keyup', handleKeyUp)
}
if (show) {
root.setAttribute('inert', 'true')
document.body.style.overflow = 'hidden'
document.activeElement.blur?.() // ! CULPRIT
}
return () => {
root.removeAttribute('inert')
document.body.style.overflow = 'auto'
window.removeEventListener('keyup', handleKeyUp)
}
}, [show, close])
return (
<>
{show && (
<Portal className={className}>
<div
ref={backdrop}
onClick={handleBackdropClick}
onTransitionEnd={handleTransitionEnd}
className={clsx('backdrop', show && 'active')}>
<div className="content">{children}</div>
</div>
</Portal>
)}
</>
)
}
Custom Textfield
const TextField = React.forwardRef(
({ label, className, ...props }, ref) => {
return (
<div className={clsx('textfield', className)}>
{label && <label>{label}</label>}
<input ref={ref} {...props} />
</div>
)
}
)
I was inlining the close function, so the useEffect hook got triggered every time close changed when the component was rendered again due to state changes and thus calling the activeElement.blur() on each keystroke.
In Modal.jsx
...
React.useEffect(() => {
...
if (show) {
root.setAttribute('inert', 'true')
document.body.style.overflow = 'hidden'
document.activeElement.blur?.() // ! CULPRIT
}
...
}, [show, close]) // as dependency
...
<Modal
show={show}
close={() => setShow(false)} // this was inlined
className="some-modal"
>
...
</Modal>
TAKEAWAY
Do not inline functions
Usually there is no reason to pass a function (pointer) as dependency
I have a form in React JS with one toggle/switch. If toggle/switch is on, then two inputs appear in the screen. So i want to get user data if the user types in inputs and the toggle/switch is on and stays on. So if the user types in inputs but he toggles/switches again to off then input values get reset and when he saves the form i must get empty user data(i get the initial values). How can i achieve something like this? I'm checking in submit handler if the switch button is false and im setting the usestate to the initial values, but it doesnt work.
My code:
Form.js
import React, { useRef, useState } from "react";
import Wrapper from "./UI/Wrapper";
import Switch from '#mui/material/Switch';
import "./Form.css";
const Form = () => {
const [showCertification, setShowCertification] = useState(false);
const [enteredCodecert, setEnteredCodecert] = useState('');
const codecertRef = useRef();
const [codesteps, setCodesteps] = useState([{ value: null }]);
const codestepsRef = useRef();
const enteredCodecertIsValid = showCertification && enteredCodecert.trim() !== '';
const codecertInputIsInvalid = !enteredCodecertIsValid;
const codestepsIsValid = showCertification && codesteps.length >= 1 && codesteps.every(codestep => codestep.value !== null && codestep.value.trim() !== '');
const codestepInputIsInvalid = !codestepsIsValid;
const showCertificationHandler = (event) => {
setShowCertification(prevState => !prevState);
if (!showCertification) {
setEnteredCodecert('');
setCodesteps([{value: null}]);
}
}
const codecertChangeHandler = (event) => {
setEnteredCodecert(event.target.value);
}
const stepChangeHandler = (i, event) => {
const values = [...codesteps];
values[i].value = event.target.value;
setCodesteps(values);
}
const addStepHandler = (event) => {
event.preventDefault();
const values = [...codesteps];
values.push({ value: null });
setCodesteps(values);
}
const removeStepHandler = (i, event) => {
event.preventDefault();
const values = [...codesteps];
values.splice(i, 1);
setCodesteps(values);
}
const submitHandler = (event) => {
event.preventDefault();
if (!enteredCodecertIsValid && showCertification) {
codecertRef.current.focus();
return;
}
if (!codestepsIsValid && showCertification) {
if (codesteps.length >= 1) {
codestepsRef.current.focus();
return;
}
return;
}
if (showCertification === false) {
setEnteredCodecert('');
setCodesteps([{value: null}]);
}
console.log(enteredCodecert);
console.log(codesteps);
}
return (
<Wrapper>
<form onSubmit={submitHandler}>
<fieldset className={`${(showCertification && codecertInputIsInvalid) || (showCertification && codestepInputIsInvalid) ? 'govgr-form-group__error' : '' }`}>
<legend><h3 className="govgr-heading-m">Certifications</h3></legend>
<Switch id="certification" checked={showCertification} onClick={showCertificationHandler} inputProps={{ 'aria-label': 'controlled' }} />
<label className="govgr-label govgr-!-font-weight-bold cert-label" htmlFor="certification">Certification</label>
{showCertification && (
<div>
<div className="govgr-form-group">
<label className="govgr-label govgr-!-font-weight-bold" htmlFor="codecert">Code Certification*</label>
{codecertInputIsInvalid && <p className="govgr-error-message"><span className="govgr-visually-hidden">Λάθος:</span>Code Certification is required.</p>}
<input className={`govgr-input govgr-!-width-three-quarter ${codecertInputIsInvalid ? 'govgr-error-input' : ''}`} id="codecert" name="codecert" type="text" value={enteredCodecert} ref={codecertRef} onChange={codecertChangeHandler} />
</div>
<div className="govgr-form-group">
<label className="govgr-label govgr-!-font-weight-bold" htmlFor="codestep">Code STEPS*</label>
{codestepInputIsInvalid && <p className="govgr-error-message"><span className="govgr-visually-hidden">Λάθος:</span>Code STEPS are required.</p>}
{codesteps.map((field, idx) => {
return (
<div key={`${field}-${idx}`}>
<div className="flex-row">
<input className={`govgr-input govgr-input--width-10 input-step ${codestepInputIsInvalid ? 'govgr-error-input' : ''}`} id="codestep" type="text" ref={codestepsRef} value={field.value || ""} onChange={e => stepChangeHandler(idx, e)} />
<button className="govgr-btn govgr-btn-warning remove-step" onClick={(e) => removeStepHandler(idx, e)}>Χ</button>
</div>
</div>
);
})}
<button className="govgr-btn govgr-btn-secondary button-step" onClick={addStepHandler}>Add Code Step</button>
</div>
</div>
)}
</fieldset>
<button className="govgr-btn govgr-btn-primary btn-center" type="submit">Save</button>
</form>
</Wrapper>
);
};
export default Form;
The issue is that in showCertificationHandler when you toggle the showCertification you are expecting the state update to be immediate.
const showCertificationHandler = (event) => {
setShowCertification(prevState => !prevState);
if (!showCertification) {
setEnteredCodecert('');
setCodesteps([{value: null}]);
}
}
This is not the case with React state updates, however. React state updates are enqueued and asynchronously processed.
To resolve, move the "reset" logic into an useEffect hook with a dependency on the showCertification state.
const showCertificationHandler = () => {
setShowCertification((prevState) => !prevState);
};
useEffect(() => {
if (!showCertification) {
setEnteredCodecert("");
setCodesteps([{ value: null }]);
}
}, [showCertification]);
For the same reason above, when resetting the states in your submitHandler they are enqueued and asynchronously processed, so console logging the state immediately after will only ever log the state values from the current render cycle, not what they will be on a subsequent render cycle. You can remove the "reset" logic from submitHandler.
const submitHandler = (event) => {
event.preventDefault();
if (!enteredCodecertIsValid && showCertification) {
codecertRef.current.focus();
return;
}
if (!codestepsIsValid && showCertification) {
if (codesteps.length >= 1) {
codestepsRef.current.focus();
return;
}
return;
}
console.log({enteredCodecert, codesteps});
};
Below, i am rendering <App/> component with children as <Input/> component array. I added few inputs using "add new" button. I am able to add input text components. But, when i am typing value in text, it is not displaying. i am not able to modify object in state array since index is showing as "-1" in setData function. Due to this, value is not showing when we type in text box. Please let me know why state is [] when i am accessing in setData function.
function Input(props)
{
return (
<div>
<label htmlFor='variable'>Name</label>
<input id='variable'
type='text'
value={props.value}
onChange={(e) => props.setData(props.id, e.target.value)} />
</div>
)
}
function App()
{
let [state, setState] = React.useState([])
let [inputs, setInputs] = React.useState([])
let setData = ((id, value) =>
{
console.log(state); // prints []
let index = state.findIndex(ele => ele.key === id);
console.log(index); // prints -1
if (!(index === -1))
{
setState(state =>
{
state[idx]["value"] = value;
})
}
})
let handleAdd = () =>
{
let idx = `${new Date().getTime()}`
let tempState = {
"key": idx,
"value": "",
}
setState(state => [...state, tempState])
let input = <Input key={tempState.key}
value={tempState.value}
id={tempState.key}
setData={setData} />
setInputs(inputs => [...inputs, input])
}
return (
<div>
<button onClick={handleAdd}>add new</button>
<div>
{inputs}
</div>
</div>
)
}
When you create an Input component inside handleAdd, it creates a closure and as a result setData gets the state that existed when the component was created, missing the newly added state.
In general, creating components and saving them to state is not a good approach. Instead it's better to only save the data onto state and render the components based on it.
Here's one way to do this, note how much simpler the component and its logic are.
function App() {
let [state, setState] = React.useState([]);
let setData = (id, value) => {
const newState = state.map((st) => {
if (st.key === id) {
st.value = value;
}
return st;
});
setState(newState);
};
const addInput = () => {
const idx = `${new Date().getTime()}`;
setState([...state, { key: idx, value: '' }]);
};
return (
<div>
<button onClick={addInput}>add new</button>
<div>
{state.map((st) => (
<Input value={st.value} key={st.key} setData={setData} id={st.key} />
))}
</div>
</div>
);
}
I want to remove a div element on component unmount using react.
I create a div with id portal in usecallback method. I want to remove it on component unmount how can I do it.
below is my code,
function Dialog () {
const [portal, setPortal] = React.useState<HTMLDivElement | null>(
(document.getElementById('portal') as HTMLDivElement) || null
);
const createPortalIfNotExists = React.useCallback(() => {
if (portal === null) {
const el = document.createElement('div');
el.id = 'portal';
document.body.appendChild(el);
setPortal(document.getElementById(
'portal'
) as HTMLDivElement);
}
}, [portal]);
createPortalIfNotExists();
if (portal === null) {
return null;
}
return ReactDOM.createPortal(
<>
<div>
{children}
</div>
</>,
portal
);
}
I have two questions here, can useEffect be instead of usecallback in this case. and how to remove the div with id portal on component unmount.
Could someone help me with this?
By using the React.useEffect internal return method, you can do it. for example:
function Dialog () {
const [portal, setPortal] = React.useState<HTMLDivElement | null>(
(document.getElementById('portal') as HTMLDivElement) || null
);
const createPortalIfNotExists = React.useCallback(() => {
if (portal === null) {
const el = document.createElement('div');
el.id = 'portal';
document.body.appendChild(el);
setPortal(document.getElementById(
'portal'
) as HTMLDivElement);
}
}, [portal]);
React.useEffect(() => {
createPortalIfNotExists();
return () => {
const portalElement = portal || document.getElementById('portal')
portal.remove();
}
}, [])
if (portal === null) {
return null;
}
return ReactDOM.createPortal(
<>
<div>
{children}
</div>
</>,
portal
);
``
I made this Component it sends props to checkbox and range components. When I was testing functionality of this 2 components I saw when I made a change in range component checkbox also rerender but it wasn't changed and the same when I change checkbox renage rerender.
Problem
When I change range comp the second one also rerender
const GeneratePassword = () => {
// Checkbox
const [state, setState] = useState({
Symbols: false,
capiatalLetters: false,
digits: false,
})
const handleChange = (e) => {
setState({ ...state, [e.target.name]: e.target.checked })
}
// Range
const [value, setValue] = useState(8)
const handleInputChange = (event) => {
setValue(event.target.value === '' ? '' : Number(event.target.value))
}
const handleBlur = () => {
if (value < 8) {
setValue(8)
} else if (value > 30) {
setValue(30)
}
}
return (
<Comp.StyledCheckboxContainer>
<Comp.CheckboxBorder>
<CheckboxContainer isCheck={handleChange} option={state} />
<Range
value={value}
handleInputChange={handleInputChange}
handleBlur={handleBlur}
setValue={setValue}
/>
</Comp.CheckboxBorder>
</Comp.StyledCheckboxContainer>
)
}
export default GeneratePassword
Components are updated on each state change because their props change. Functions that are used as callbacks should be memoized to prevent this. That some of them rely on current state is a problem, they need to use updater function to avoid referring to stale state:
const handleChange = useCallback((e) => {
setState((state) => ({ ...state, [e.target.name]: e.target.checked }))
}, [])
...
const handleInputChange = useCallback((event) => {
setValue(event.target.value === '' ? '' : Number(event.target.value))
}, [])
...
const handleBlur = useCallback(() => {
setValue(value => {
if (value < 8) {
return 8
} else if (value > 30) {
return 30
}
})
}, [])
At this point child components are able to prevent unnecessary rerenders. If they don’t do this, they need to be additionally wrapped with React.PureComponent or React.memo. For arbitrary component a wrapper can be:
OptimizedComp = React.memo(props => <Comp {...props} />);
It is because your callback for Checkbox component is defined inside parent which re-renders whenever range component changes causing a change is callback for Checkbox component since it gets new value on every render.
const handleInputChange = (event) => {
setValue(event.target.value === '' ? '' : Number(event.target.value))
}
On every render, handleInputChange gets a new value, a new reference. You need to use React.useCallbak() to keep the same value for all renders
const handleInputChange = React.useCallback((event) => {
setValue(event.target.value === '' ? '' : Number(event.target.value))
}, []);