I'm trying to make an overridable usage of a useReducer in a Component for specific use cases.
Following my overfly simplified working state of work :
function defaultToggleReducer (state, action) {
console.log('Default toggler used')
return !state
}
function customTogglerReducer (state, action) {
console.log('Custom toggler used')
return !state
}
// Dummy wrap to log the initialization from `Togller` component to point the twice initializations
function toggleInitializer (state) {
console.log('toggleInitializer')
return state
}
function Toggler (props) {
const [
toggleState,
toggleDispatch,
] = React.useReducer(defaultToggleReducer, false, toggleInitializer)
// Here is the prt making the previous `useReducer` useless
const state = props.toggleState !== undefined ? props.toggleState : toggleState
const dispatch = props.toggleDispatch || toggleDispatch
return (
<button
type="button"
onClick={() => dispatch({ type: 'add' })}>
{Boolean(state).toString()}
</button>
)
}
function App () {
const [customToggleState, customToggleDispatch] = React.useReducer(
customTogglerReducer,
false
)
return (
<div>
<fieldset>
<legend>Default</legend>
<Toggler />
</fieldset>
<fieldset>
<legend>Customized</legend>
<Toggler
toggleState={customToggleState}
toggleDispatch={customToggleDispatch} />
<button
type="button"
onClick={() => customToggleDispatch({ type: 'parentAction' })}>
This is why I want dispatch
</button>
</fieldset>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root" />
Here, there is something smell bad, I have no doubts about this is working but the fact where useReducer is called in Toggler even if not used let me suspicious. So, it's a dead reducer I think.
So my question is:
Did you have a way to achieve a reduce control from parent component properly (at least: better than this) ?
Use components' composition to solve this problem. Break Toggler to two components - dumb Toggler that expects an outside reducer, and DefaultToggler that renders Toggler, and supplies the reducer.
If you need a standard toggler, use DefaultToggler, and if you need a custom version use the dumb Toggler, and supply a custom reducer.
function defaultToggleReducer (state, action) {
console.log('Default toggler used')
return !state
}
function customTogglerReducer (state, action) {
console.log('Custom toggler used')
return !state
}
// Dummy wrap to log the initialization from `Togller` component to point the twice initializations
function toggleInitializer (state) {
console.log('toggleInitializer')
return state
}
function Toggler ({ toggleState: state, toggleDispatch: dispatch }) {
return (
<button
type="button"
onClick={() => dispatch({ type: 'add' })}>
{Boolean(state).toString()}
</button>
)
}
function DefaultToggler () {
const [
toggleState,
toggleDispatch,
] = React.useReducer(defaultToggleReducer, false, toggleInitializer)
return (
<Toggler
toggleState={toggleState}
toggleDispatch={toggleDispatch}
/>
);
};
function App () {
const [customToggleState, customToggleDispatch] = React.useReducer(
customTogglerReducer,
false
)
return (
<div>
<fieldset>
<legend>Default</legend>
<DefaultToggler />
</fieldset>
<fieldset>
<legend>Customized</legend>
<Toggler
toggleState={customToggleState}
toggleDispatch={customToggleDispatch} />
<button
type="button"
onClick={() => customToggleDispatch({ type: 'parentAction' })}>
This is why I want dispatch
</button>
</fieldset>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root" />
Related
return (
<div>
{
countries.map((country)=>
<div key={country.ccn3}>
{country.name.common}<button className="button" value={country} onClick={ChangeDisplay}>show</button>
</div>
)
}
</div>
);
}
as the code show: how do you get the value of "button", in my case, value should be "country" obj, which can be rendered with onClick function "ChangeDisplay".
Data attributes are a good way.
For a button:
<button data-value={country} onClick={changeDisplay}>show</button>
Click handler:
const changeDisplay = (e) => {
const { value } = e.target.dataset;
console.log(value);
};
You can simply get the value from e.target.value like how you do with form fields. Value needs to be a string.
const App = () => {
const onClick = (e) => alert(e.target.value)
return (<button onClick={onClick} value="my button value">MyButton</button>)
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
If you want to pass an Object. Then you can do without event by simply passing the object to the function.
<button onClick={()=> ChangeDisplay(country)} />
I tried to put a form in a separate reusable component but when used that way I can't type anything into the input. I observed, that after entering one letter (it does not appear in the input box) it seems that React rerender the whole component and the name is updated with the inserted letter.
in the version 2 the same code works correctly.
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = key => event => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = event => {
event.preventDefault();
handleChange();
};
// VERSION 1. doesn't work
const FormEdit = () => (
<form>
<div className="form-group">
<input onChange={handleChange("name")} type="text"/>
</div>
<button onClick={submitEdit}> Submit </button>
</form>
)
return (
<Layout>
<div>
{name} //<-it shows only one letter
<FormEdit />
</div>
</Layout>
);
// VERSION 2 -> works properly
return (
<Layout>
<div>
{name} //<-the updated name is shown immediately
<form>
<div className="form-group">
<input onChange={handleChange("name")} type="text"/>
</div>
<button onClick={submitEdit}> Submit </button>
</form>
</div>
</Layout>
);
};
export default User;
The issue is directly related to declaring the FormEdit component within the other component. Here's why:
In a functional component, everything declared inside gets destroyed and re-created each render. It's no different than a normal function call. This is what makes React's hooks so special. They keep track of values in between renders and make sure they are re-created with the correct values.
You're declaring the FormEdit component inside a function, which means not only is it re-declared every render, but as a side-effect it also un-mounts and remounts each render as well.
This has a few different effects:
The component's input loses focus every render.
It's impossible for it to maintain its own state.
It's not very performant.
Below is a working example to demonstrate.
const {useState, useEffect} = React;
const Example = () => {
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = (key) => (event) => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = (event) => {
event.preventDefault();
handleChange();
};
const FormEdit = () => {
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
return (
<form>
<div>
<input onChange={handleChange("name")} type="text"/>
</div>
<button onClick={submitEdit}> Submit </button>
</form>
)
}
return (
<div>
{name}
<FormEdit />
</div>
);
}
ReactDOM.render(<Example />, 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>
As for why you only see the first character; You are not giving the input a value, only an onChange. If the component does not unmount, this just makes it an "uncontrolled" component. The input still gets it's value updated, you just can't programatically control it. But, since it is unmounting and re-mounting every render, it loses its last value every time the user types.
Making it a controlled input would fix this:
const {useState, useEffect} = React;
const Example = () => {
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = (key) => (event) => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = (event) => {
event.preventDefault();
handleChange();
};
const FormEdit = () => {
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
return (
<form>
<div>
<input value={name} onChange={handleChange("name")} type="text"/>
// ^ Add this
</div>
<button onClick={submitEdit}> Submit </button>
</form>
)
}
return (
<div>
{name}
<FormEdit />
</div>
);
}
ReactDOM.render(<Example />, 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>
This is a little better, but still not ideal. Now it keeps the value each update, but it still loses focus. Not a very good user experience.
This final solution is to never declare a component within another component.
const {useState, useEffect} = React;
const FormEdit = (props) => {
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
return (
<form>
<div>
<input value={props.name} onChange={props.handleChange("name")} type="text"/>
</div>
<button onClick={props.submitEdit}> Submit </button>
</form>
)
}
const Example = () => {
// the part same for the both versions
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = (key) => (event) => {
setUser({
...userdata,
[ key ]: event.target.value
});
};
const submitEdit = (event) => {
event.preventDefault();
handleChange();
};
return (
<div>
{name}
<FormEdit name={name} handleChange={handleChange} submitEdit={submitEdit} />
</div>
);
}
ReactDOM.render(<Example />, 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>
Now it only mounts once, keeps focus, and updates as expected.
You would have to pass your form handlers to the child component as props so that the lifted state can be manipulated from the child.
// Parent Component
...
const [userdata, setUser] = useState({});
const { name } = userdata
const handleChange = key => event => {
...
};
const submitEdit = event => {
...
};
return (
<Layout>
<div>
{name}
<FormEdit handleChange={handleChange} submitEdit={submitEdit}/>
</div>
</Layout>
);
and then in the child:
// Child Component
const FormEdit = (props) => (
<form>
<div className="form-group">
<input onChange={props.handleChange("name")} type="text"/>
</div>
<button onClick={props.submitEdit}> Submit </button>
</form>
)
Your FormEdit component which is inside the App component is causing the entire App component to re-render when the state gets updated onChange and hence you can only enter only one character at a time. It is generally not a great idea to declare a component within a component. Refer this link for more info. All you have to do is pull the FormEdit component out of the App component in its own separate function and pass the change handlers as props to the FormEdit component. Have a look at the working code below.
import React, { useState } from 'react';
const FormEdit = ({ handleChange, submitEdit, name }) => {
return (
<form>
<div className='form-group'>
<input onChange={handleChange('name')} type='text' value={name || ''} />
</div>
<button onClick={submitEdit} type='submit'>
Submit
</button>
</form>
);
};
export default function App() {
const [userdata, setUser] = useState();
const { name } = userdata || {};
const handleChange = key => event => {
setUser(prevState => {
return { ...prevState, [key]: event.target.value };
});
event.persist();
event.preventDefault();
};
const submitEdit = event => {
event.preventDefault();
handleChange();
};
return (
<div>
<div>
{name || ''}
<FormEdit
handleChange={handleChange}
submitEdit={submitEdit}
name={name}
/>
</div>
</div>
);
}
const Child1 = (props) => {
const [obj, setObj] = React.useState({count: 1, enabled: true})
const onButtonClick = () => {
setObj({...obj, count: obj.count+1})
}
const onDelayedIncrement = () => {
setTimeout(() => {
setObj({...obj, count: obj.count+1})
}, 3000)
}
return (
<div>
<div>{obj.count}</div>
<button onClick={onButtonClick}>Increment</button>
<div><button onClick={onDelayedIncrement}>Delayed Increment</button></div>
</div>
);
};
ReactDOM.render(
<Child1 />,
document.getElementById('root')
);
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
In the above code if we click Delayed increment and later if we keep on clicking Increment, after setTimeout is executed and when setState is called it is using old state. How to solve this problem?
Use the functional form of setState:
setObj(currentObj => ({...currentObj, count: currentObj.count+1}))
More info in the official documentation.
Hooks related documentation.
I am trying to render a component in App.js based on a MobX observable value. I want to define an action that takes a component and some new custom prop assignments. Here's what I have now:
// App.js inside render()
{ ui_store.main_area_sequence }
.
// ui_store.js
// OBSERVABLES / state
main_area_sequence = <ChoosePostType />;
// ACTIONS / state mutators
update_ui_in_main_section(ComponentTag, props) {
this.main_area_sequence = <ComponentTag {props} />
}
The component argument works as intended. But I can't get the props argument working. Ideally I would use it to build something like:
<ChildComponentToBeSwitchedIn name={ui_store.name} handleClick={this.handleClick} />
A button in <ChoosePostType /> might reassign the observable value in main_area_sequence to the above on click, as one example.
Does anyone know how to pass prop assignments as an argument in order to do this?
I suspect you're overthinking this.
Recall that JSX is just JavaScript. When you type this:
<Foo bar={123}>Baz!</Foo>
...you're really writing this:
React.createElement(Foo, { bar: 123 }, 'Baz!');
What if the component you want to render is dynamic, though? That's fine; it still works. For example, suppose we kept the component and props in our React component's state:
render() {
return (
<this.state.dynamicComponent
{...this.state.dynamicComponentProps}
name={this.props.name}
onClick={this.handleClick}
/>
);
}
The above JSX translates to this JavaScript:
React.createElement(this.state.dynamicComponent,
{ ...this.state.dynamicComponentProps,
name: this.props.name,
onClick: this.handleClick,
}
);
As you can see, we get props from this.state.dynamicComponentProps, but we also add a name prop from this.props.name and an onClick prop from this.handleClick. You can imagine this.handleClick calling this.setState to update dynamicComponent and dynamicComponentProps.
Actually, you don't have to imagine it, because you can see it working in the below snippet:
class DynamicComponent extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = {
dynamicComponent: ShowMeFirst,
dynamicComponentProps: { style: { color: 'blue' }},
};
}
render() {
return (
<this.state.dynamicComponent
{...this.state.dynamicComponentProps}
name={this.props.name}
onClick={this.handleClick}
/>
);
}
handleClick() {
this.updateDynamicComponent(ShowMeSecond, { style: { color: 'red' }});
}
updateDynamicComponent(component, props) {
this.setState({
dynamicComponent: component,
dynamicComponentProps: props,
});
}
}
const ShowMeFirst = ({ name, style, onClick }) => (
<button type="button" style={style} onClick={onClick}>
Hello, {name}!
</button>
);
const ShowMeSecond = ({ name, style, onClick }) => (
<button type="button" style={style} onClick={onClick}>
Goodbye, {name}!
</button>
);
ReactDOM.render(<DynamicComponent name="Alice"/>, document.querySelector('div'));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div></div>
I've used plain old React state above, but the same thing works with MobX, as you can see below. (I've never used MobX before, so my code might be less than idiomatic, but hopefully you'll get the idea.)
const { action, decorate, observable } = mobx;
const { observer } = mobxReact;
class UIStore {
dynamicComponent = ShowMeFirst;
dynamicComponentProps = { style: { color: 'blue' }};
updateDynamicComponent(component, props) {
this.dynamicComponent = component;
this.dynamicComponentProps = props;
}
}
decorate(UIStore, {
dynamicComponent: observable,
dynamicComponentProps: observable,
updateDynamicComponent: action,
});
const DynamicComponent = observer(({ store, name }) => (
<store.dynamicComponent
{...store.dynamicComponentProps}
name={name}
onClick={() =>
store.updateDynamicComponent(ShowMeSecond, { style: { color: 'red' }})
}
/>
));
const ShowMeFirst = ({ name, style, onClick }) => (
<button type="button" style={{...style}} onClick={onClick}>
Hello, {name}!
</button>
);
const ShowMeSecond = ({ name, style, onClick }) => (
<button type="button" style={{...style}} onClick={onClick}>
Goodbye, {name}!
</button>
);
ReactDOM.render(<DynamicComponent name="Alice" store={new UIStore()} />, document.querySelector('div'));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/mobx/lib/mobx.umd.js"></script>
<script src="https://unpkg.com/mobx-react#5.2.3/index.js"></script>
<div></div>
I had to use decorate here since Stack Overflow's Babel configuration doesn't include decorators, but I've posted the same thing with decorators on CodeSandbox: https://codesandbox.io/s/qlno63zkw4
If you aren't passing props, then don't include a props parameter.
update_ui_in_main_section(ComponentTag) {
this.main_area_sequence = <ComponentTag handleClick={this.handleClick} />;
}
I'd like to create a stateless Search component. When the button is clicked, I need to access the value of the search field. Is the following an acceptable way to do this? Are they any drawbacks to this approach?
const Search = ({onBtnClick}) => {
const onClick = (e) => {
const query = e.target.previousSibling.value;
onBtnClick(query);
};
return(
<div>
<input type="search" />
<button onClick={onClick}>Search</button>
</div>
)
}
In stateless components you can use ref with function, like so
const Search = ({ onBtnClick }) => {
let search;
const setNode = (node) => {
search = node;
};
const onClick = () => {
onBtnClick(search.value);
};
return (
<div>
<input type="search" ref={ setNode } />
<button onClick={ onClick }>Search</button>
</div>
);
}
function onBtnClick(value) {
console.log(value);
}
ReactDOM.render(
<Search onBtnClick={onBtnClick} />,
document.getElementById('container')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container"></div>
I'm not connoisseur of react, but i think that your approach is OK,
because in official react documentation i saw NOT one time something like that {value: event.target.value}:
Here we have component with state, and have this:
this.setState({value: event.target.value});
but i think in your component without state it will be OK use:
let query = e.target.previousSibling.value;
onBtnClick(query);
PS: I also like Alexander T. answer.)