I'm trying to implement MVVM in React (requirement from the class I'm taking). I'm using functional components for the view and have typescript classes for the ViewModel. My components do not re-render when a property is updated in the ViewModel though.
Here's a simple example for a page that should toggle between a login and sign up form. The setCurrentForm gets called correctly and the value in the ViewModel does update, but it doesn't change the View.
// AuthView.tsx
const AuthView: React.FC = () => {
const VM = new AuthViewModel();
let form;
if (VM.currentForm === FORMS.SignUp) {
// Toggles the current form between FORMS.SignUp and FORMS.Login
form = <SignUpForm setCurrentForm={() => VM.setCurrentForm()} />
} else {
form = <LoginForm setCurrentForm={() => VM.setCurrentForm()} />
}
return (
<Container>
{/* Sign up card */}
<div className="mt-12">
{form}
</div>
</Container>
);
}
export default AuthView;
// AuthViewModel.tsx
export default class AuthViewModel {
email: string = "";
password: string = "";
currentForm: FORMS = FORMS.SignUp;
setCurrentForm() {
console.log("Setting form in VM");
if (this.currentForm === FORMS.SignUp) {
console.log("Switching to login")
this.currentForm = FORMS.Login;
} else if (this.currentForm === FORMS.Login) {
console.log("Switching to signup")
this.currentForm = FORMS.SignUp;
}
}
}
I could force the re-render with a hook by updating an arbitrary value, but I think there's a better way to do this. What are your thoughts?
You might be missunderstanding how react components re-render, just because you change some property in another object it has no bearing on the component itself, even if it has taken a property from this object.
Hooks are directly connected to the reacts render mechanism and can trigger render cycles, as such you should use something like this:
const AuthView: React.FC = () => {
// if you don't put this in a state a new VM will be created when the component rerenders
const [VM] = useState(new AuthViewModel());
useEffect(() => {
// Maybe some handler code is needed?
}, VM.currentForm);
let form;
if (VM.currentForm === FORMS.SignUp) {
// Toggles the current form between FORMS.SignUp and FORMS.Login
form = <SignUpForm setCurrentForm={() => VM.setCurrentForm()} />
} else {
form = <LoginForm setCurrentForm={() => VM.setCurrentForm()} />
}
return (
<Container>
{/* Sign up card */}
<div className="mt-12">
{form}
</div>
</Container>
);
}
export default AuthView;
I've never tried to observe a nested property via a hook, so not 100% this works.
EDIT: it doesn't work, but it makes sense, the rendering call gets triggered when you actually call the set function of the useState hook, not really sure how to implement this pattern with hooks and without something like redux or mobx, but here is my best approach:
class AuthViewModel() {
constructor(public readonly currentForm = 'LOGIN');
public setCurrentForm = () => {
if(this.currentForm === 'LOGIN')
return new AuthViewModel('SIGNUP')
else
return new AuthViewModel(); // will default to login
}
}
and then the component
const AuthView: React.FC = () => {
// if you don't put this in a state a new VM will be created when the component rerenders
const [VM, setVM] = useState(new AuthViewModel());
let form;
if (VM.currentForm === FORMS.SignUp) {
// Toggles the current form between FORMS.SignUp and FORMS.Login
form = <SignUpForm setCurrentForm={() => setVM(VM.setCurrentForm())} />
} else {
form = <LoginForm setCurrentForm={() => setVM(VM.setCurrentForm())} />
}
return (
<Container>
{/* Sign up card */}
<div className="mt-12">
{form}
</div>
</Container>
);
}
export default AuthView;
What you have here doesn't feel very React. For starters, I've only rarely seen classes used outside of class-based components. I'm just going to spitball a different solution here that might not exactly match what you need, but hopefully gets you going in a correct direction.
const Authenticate: FC = props => {
const [mode, setMode] = useState<"login" | "create">("login");
return (
<div>
{mode === "login" && <Login onLogin={({email, password}) => {/*login handler logic*/}}/>}
{mode === "create" && <CreateAccount onCreate={({email, password}) => {/*create handler logic*/}}/>}
<button
disabled={mode === "login"}
onClick={() => setMode("login")}
>
login
</button>
<button
disabled={mode === "create"}
onClick={() => setMode("create")}
>
sign up
</button>
</div>
)
}
const Login: FC<{onLogin: ({email: string, password: string}) => any}> = props => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const { onLogin } = props;
return (
<form onSubmit={() => onLogin({email, password})}>
<input value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button type="submit">Login</button>
</form>
);
}
const CreateAccount: FC<{onCreate: ({email: string, password: string}) => any}> = props => {
return (
<div>... similar to <Login/> ... </div>
)
}
Related
This is my project for business card app.
I have a problem with using state and props between components.
Component tree looks like this.
Editor <- CardEditForm <- ImageFileInput
The url state is in the Editor component. There is a function to update state and give it as props to child components. When I upload an image on ImageFileInput component, url data goes up until the editor component and then using setUrl to url be updated. And then I gave url to CardEditorForm component.
The problem is this, In cardEditorForm, when it comes to using url props, I can't get the updated url. Only gets the initial state. I really need to get an updated url. I also tried to use setTimeout() to get the updated url. But it doesn't work either. What can I do?..
It's my first time to ask a question on stack overflow. Thank you for helping the newb.
Here is the code.
editor.jsx
const Editor = ({ cards, deleteCard, createOrUpdateCard }) => {
const [url, setUrl] = useState('');
const updateUrl = (src) => {
setUrl(src);
};
return (
<section className={styles.editor}>
<h1 className={styles.title}>Card Maker</h1>
{Object.keys(cards).map((key) => (
<CardEditForm
key={key}
card={cards[key]}
onDelete={deleteCard}
onUpdate={createOrUpdateCard}
updateUrl={updateUrl}
url={url}
/>
))}
<CardAddForm onAdd={createOrUpdateCard} updateUrl={updateUrl} url={url} />
</section>
);
};
card_edit_form.jsx
const CardEditForm = ({ card, onDelete, onUpdate, updateUrl, url }) => {
// ...
const changeUrl = () => {
setTimeout(() => {
const newCard = {
...card,
fileURL: url,
};
onUpdate(newCard);
}, 4000);
};
return (
<form className={styles.form}>
// ...
<div className={styles.fileInput}>
<ImageFileInput updateCard={changeUrl} updateUrl={updateUrl} />
</div>
// ...
</form>
);
};
export default CardEditForm;
image_file_input.jsx
const ImageFileInput = ({ updateUrl, updateCard }) => {
const [image, setImage] = useState('');
const upload = new Upload();
const onUpload = (e) => {
e.preventDefault();
upload.uploadImage(image).then((data) => updateUrl(data));
updateCard(e);
};
return (
<div>
<input type="file" onChange={(e) => setImage(e.target.files[0])} />
<button name="fileURL" onClick={onUpload}>
image
</button>
</div>
);
};
I'm trying to build an input component with a clear button using react#17
import { useRef } from 'react';
const InputWithClear = props => {
const inputRef = useRef();
return (
<div>
<input
ref={inputRef}
{...props}
/>
<button
onClick={() => {
inputRef.current.value = '';
inputRef.current.dispatchEvent(
new Event('change', { bubbles: true })
);
}}
>
clear
</button>
</div>
);
};
using this component like:
<InputWithClear value={value} onChange={(e) => {
console.log(e); // I want to get a synthetic event object here
}} />
but the clear button works once only when I did input anything first, and stop working again.
if I input something first and then click the clear button, it does not work.
why not using?
<button
onClick={() => {
props.onChange({
target: { value: '' }
})
}}
>
clear
</button>
because the synthetic event object will be lost
So, how do I manually trigger a synthetic change event of a react input component?
Try this approach,
Maintain state at the parent component level (Here parent component is App), onClear, bubble up the handler in the parent level, and update the state.
import React, { useState } from "react";
import "./styles.css";
const InputWithClear = (props) => {
return (
<div>
<input {...props} />
<button onClick={props.onClear}>clear</button>
</div>
);
};
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<InputWithClear
value={value}
onChange={(e) => {
console.log(e); // I want to get a synthetic event object here
setValue(e.target.value);
}}
onClear={() => {
setValue("");
}}
/>
</div>
);
}
Working code - https://codesandbox.io/s/youthful-euler-gx4v5?file=/src/App.js
you should use state to control input value rather than create useRef, that's the way to go. you can use a stopPropagation prop to control it:
const InputWithClear = ({value, setValue, stopPropagation = false}) => {
const onClick = (e) => {
if(stopPropagation) e.stopPropagation()
setValue('')
}
return (
<div>
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
<button
onClick={onClick}
>
clear
</button>
</div>
);
};
export default function App() {
const [value, setValue] = useState('')
return (
<div className="App">
<InputWithClear value={value} setValue={setValue} stopPropagation />
</div>
);
}
I want to set a Error Message under my Input Fields, if the value isn't valid. The message will always be the same. But the message is shown on every Input field, if the value isn't valid. That's logic if you see the code. But how can I make it work?
And yes, I have to use the error state in the Parent component because I have lifted the state up. Because of style dependencies of the error prop. I'm using styled-components for that.
Parent component:
const List = (props) => {
const [error, setError] = useState('');
function handleChange(event) {
const currentValue = parseInt(event.target.value, 10);
if (currentValue > 10) {
setError('Error');
} else setError(null);
}
return (
{items.map(item => (
<MyInput
value={item.value}
key={item.input}
error={error}
handleChange={e => handleChange(e)}
/>
))}
);
};
export default List;
The child Component:
const MyInput = (props) => {
return (
<>
<input
defaultValue={props.value}
type="text"
placeholder={props.value}
onChange={e => props.handleChange(e)}
/>
{props.error ? {props.error} : null}
</>
);
};
export default MyInput;
Is it possible to use the state in the parent component?
I hope you can help me :)
I'm not yet a React master, hence my question. Why there is still invoking a parent function if in child component I'm writing new characters in input fields? I want to call parent method only when I clicked Search button in my child component.
Parent component:
class MainPage extends Component {
render() {
let searchOffersBar = (
<MuiThemeProvider>
<SearchOffer
offersFound={this.props.onOffersFound}
/>
</MuiThemeProvider>
);
let searchResults = (
<SearchResults
offers={this.props.offers}
/>
);
return (
<Aux>
<div className={classes.container}>
<Intro/>
<div className={classes.contentSection}>
{searchOffersBar}
{searchResults}
</div>
</div>
</Aux>
)
}
}
const mapStateToProps = state => {
return {
offers: state.offers.offers
}
}
const mapDispatchToProps = dispatch => {
return {
onOffersFound: (searchParams) => dispatch(actions.fetchOffersByCriteria(searchParams))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(MainPage);
<SearchOffer> is my child component with a search section (input fields and button "Search offers"). I want to fill some data in my inputs and then click the button. I though that clicking the button will invoke a method in child component: onOffersFound:
const searchOffer = props => {
let currentDate = new Date();
const [searchCriteria, setSearchCriteria] = useState({
brand: 'xxx',
capacity: 100
})
const [drawerIsOpen, setDrawerIsOpen] = useState(false);
const handleToggle = () => setDrawerIsOpen(!drawerIsOpen);
const handleBrand = (event) => {
let mergedState = updateObject(searchCriteria, {brand: event.target.value})
setSearchCriteria(mergedState);
}
const handleCapacity = (event) => {
let mergedState = updateObject(searchCriteria, {capacity: event.target.value});
setSearchCriteria(mergedState);
}
const handleBookingFrom = (bookingFromValue) => {
let mergedState = updateObject(searchCriteria, {bookingFrom: bookingFromValue});
setSearchCriteria(mergedState);
}
const handleBookingTo = (bookingToValue) => {
let mergedState = updateObject(searchCriteria, {bookingTo: bookingToValue});
setSearchCriteria(mergedState);
}
return (
<div className={classes.sideNav}>
<Button variant={"outlined"} onClick={handleToggle} className={classes.sideNavBtn}>Search</Button>
<Drawer
className={classes.drawer}
containerStyle={{top: 55}}
docked={false}
width={200}
open={drawerIsOpen}
onRequestChange={handleToggle}
>
<AppBar title="Search"/>
<form noValidate autoComplete="off" onSubmit={props.offersFound(searchCriteria)}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<Grid container justify="space-around">
<TextField
id="brand"
label="Brand"
margin="normal"
onChange={handleBrand}
/>
<TextField
id="capacity"
label="Capacity"
margin="normal"
onChange={handleCapacity}
/>
<Button variant="contained" color="primary">
Search
</Button>
</Grid>
</MuiPickersUtilsProvider>
</form>
</Drawer>
</div>
);
}
export default searchOffer;
onOffersFound in my action creator looks like:
export const fetchOffersByCriteria = (searchParams) => {
return dispatch => {
let queryParams = '?brand='+searchParams.brand + '&capacity='+searchParams.capacity;
axios.get('/getFilteredOffers' + queryParams)
.then(response => {
dispatch(saveFoundOffers(response.data)); --> saves the state
})
.catch(error => {
console.log(error);
})
}
}
My question is why the above method fetchOffersByCriteria is invoked every time I enter new character in my child component? I want to invoke this method only when I click the Search button in child component. Maybe my approach is bad?
Thanks for all tips!
The issue is that props.offersFound(searchCriteria) is being invoked every render. The onSubmit prop should be a function to be invoked when submitted. Currently, it's being invoked immediately.
This line:
onSubmit={props.offersFound(searchCriteria)}
Should be (or something similar):
onSubmit={() => props.offersFound(searchCriteria)}
Currently, when typing in the brand (or capacity) field, the handleBrand change callback is invoked. This invokes setSearchCriteria (a state update) which triggers a re-render of the component. While this component is re-rendering, it's immediately invoking props.offersFound(searchCriteria) and passing the return value to the onSubmit prop. You likely want the onSubmit prop to be a function to be invoked at the time of submitting.
See the documentation for controlled components for more de3tails.
<form
noValidate
autoComplete="off"
onSubmit={props.offersFound(searchCriteria)}>
You are immediately invoking prop and trying to use result returned as event listener. It should be
<form
noValidate
autoComplete="off"
onSubmit={() => props.offersFound(searchCriteria)}>
instead
I'm looking for the easiest solution to pass data from a child component to his parent.
I've heard about using Context, pass trough properties or update props, but I don't know which one is the best solution.
I'm building an admin interface, with a PageComponent that contains a ChildComponent with a table where I can select multiple line. I want to send to my parent PageComponent the number of line I've selected in my ChildComponent.
Something like that :
PageComponent :
<div className="App">
<EnhancedTable />
<h2>count 0</h2>
(count should be updated from child)
</div>
ChildComponent :
const EnhancedTable = () => {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Click me {count}
</button>
)
};
I'm sure it's a pretty simple thing to do, I don't want to use redux for that.
A common technique for these situations is to lift the state up to the first common ancestor of all the components that needs to use the state (i.e. the PageComponent in this case) and pass down the state and state-altering functions to the child components as props.
Example
const { useState } = React;
function PageComponent() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1)
}
return (
<div className="App">
<ChildComponent onClick={increment} count={count} />
<h2>count {count}</h2>
(count should be updated from child)
</div>
);
}
const ChildComponent = ({ onClick, count }) => {
return (
<button onClick={onClick}>
Click me {count}
</button>
)
};
ReactDOM.render(<PageComponent />, 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>
You can create a method in your parent component, pass it to child component and call it from props every time child's state changes, keeping the state in child component.
const EnhancedTable = ({ parentCallback }) => {
const [count, setCount] = useState(0);
return (
<button onClick={() => {
const newValue = count + 1;
setCount(newValue);
parentCallback(newValue);
}}>
Click me {count}
</button>
)
};
class PageComponent extends React.Component {
callback = (count) => {
// do something with value in parent component, like save to state
}
render() {
return (
<div className="App">
<EnhancedTable parentCallback={this.callback} />
<h2>count 0</h2>
(count should be updated from child)
</div>
)
}
}
To make things super simple you can actually share state setters to children and now they have the access to set the state of its parent.
example:
Assume there are 4 components as below,
function App() {
return (
<div className="App">
<GrandParent />
</div>
);
}
const GrandParent = () => {
const [name, setName] = useState("i'm Grand Parent");
return (
<>
<div>{name}</div>
<Parent setName={setName} />
</>
);
};
const Parent = params => {
return (
<>
<button onClick={() => params.setName("i'm from Parent")}>
from Parent
</button>
<Child setName={params.setName} />
</>
);
};
const Child = params => {
return (
<>
<button onClick={() => params.setName("i'm from Child")}>
from Child
</button>
</>
);
};
so grandparent component has the actual state and by sharing the setter method (setName) to parent and child, they get the access to change the state of the grandparent.
you can find the working code in below sandbox,
https://codesandbox.io/embed/async-fire-kl197
IF we Have Parent Class Component and Child function component this is how we going to access child component useStates hooks value :--
class parent extends Component() {
constructor(props){
super(props)
this.ChildComponentRef = React.createRef()
}
render(){
console.log(' check child stateValue: ',
this.ChildComponentRef.current.info);
return (<> <ChildComponent ref={this.ChildComponentRef} /> </>)
}
}
Child Component we would create using
React.forwardRef((props, ref) => (<></>))
. and
useImperativeHandle(ref, createHandle, [deps])
to customizes the instance value that is exposed to parent components
const childComponent = React.forwardRef((props, ref) => {
const [info, setInfo] = useState("")
useEffect(() => {
axios.get("someUrl").then((data)=>setInfo(data))
})
useImperativeHandle(ref, () => {
return {
info: info
}
})
return (<> <h2> Child Component <h2> </>)
})
I had to do this in type script. The object-oriented aspect would need the dev to add this callback method as a field in the interface after inheriting from parent and the type of this prop would be Function. I found this cool!
Here's an another example of how we can pass state directly to the parent.
I modified a component example from react-select library which is a CreatableSelect component. The component was originally developed as class based component, I turned it into a functional component and changed state manipulation algorithm.
import React, {KeyboardEventHandler} from 'react';
import CreatableSelect from 'react-select/creatable';
import { ActionMeta, OnChangeValue } from 'react-select';
const MultiSelectTextInput = (props) => {
const components = {
DropdownIndicator: null,
};
interface Option {
readonly label: string;
readonly value: string;
}
const createOption = (label: string) => ({
label,
value: label,
});
const handleChange = (value: OnChangeValue<Option, true>, actionMeta: ActionMeta<Option>) => {
console.group('Value Changed');
console.log(value);
console.log(`action: ${actionMeta.action}`);
console.groupEnd();
props.setValue(value);
};
const handleInputChange = (inputValue: string) => {
props.setInputValue(inputValue);
};
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = (event) => {
if (!props.inputValue) return;
switch (event.key) {
case 'Enter':
case 'Tab':
console.group('Value Added');
console.log(props.value);
console.groupEnd();
props.setInputValue('');
props.setValue([...props.value, createOption(props.inputValue)])
event.preventDefault();
}
};
return (
<CreatableSelect
id={props.id}
instanceId={props.id}
className="w-100"
components={components}
inputValue={props.inputValue}
isClearable
isMulti
menuIsOpen={false}
onChange={handleChange}
onInputChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Type something and press enter..."
value={props.value}
/>
);
};
export default MultiSelectTextInput;
I call it from the pages of my next js project like this
import MultiSelectTextInput from "../components/Form/MultiSelect/MultiSelectTextInput";
const NcciLite = () => {
const [value, setValue] = useState<any>([]);
const [inputValue, setInputValue] = useState<any>('');
return (
<React.Fragment>
....
<div className="d-inline-flex col-md-9">
<MultiSelectTextInput
id="codes"
value={value}
setValue={setValue}
inputValue={inputValue}
setInputValue={setInputValue}
/>
</div>
...
</React.Fragment>
);
};
As seen, the component modifies the page's (parent page's) state in which it is called.
I've had to deal with a similar issue, and found another approach, using an object to reference the states between different functions, and in the same file.
import React, { useState } from "react";
let myState = {};
const GrandParent = () => {
const [name, setName] = useState("i'm Grand Parent");
myState.name=name;
myState.setName=setName;
return (
<>
<div>{name}</div>
<Parent />
</>
);
};
export default GrandParent;
const Parent = () => {
return (
<>
<button onClick={() => myState.setName("i'm from Parent")}>
from Parent
</button>
<Child />
</>
);
};
const Child = () => {
return (
<>
<button onClick={() => myState.setName("i'm from Child")}>
from Child
</button>
</>
);
};