I am creating the Screens component in one file then passing it into the Modal component. Inside Modal I am trying to use Screens. Screens takes an index prop to tell it what to display. If I display props.screens (the Screens component) it works but I don't see a way to pass in props. If I try to display (screens) => () everything freezes.
File where Screens is defined...
const Screens = (props) => {
let index = props !== undefined ? props.index.idx : 0;
const [contentData, setContentData] = useState({});
let [indexState, setIndexState] = useState(index);
console.log(`indexState: ${indexState}`)
const onChange = (e) => {
const newData = {
...contentData,
[e.target.id]: e.target.value
}
setContentData(newData)
};
const Screen1 = () => {
return (
<>
<input type="text" id="screen1_input1" onChange={onChange} />
<br />
one
</>
)
};
const Screen2 = () => {
return (
<>
<input type="text" id="screen2_input1" onChange={onChange} />
<br />
<input type="text" id="screen2_input2" onChange={onChange} />
<br />
two
</>
)
};
const content = [
Screen1,
Screen2
];
return (
<>
<h2 className="screens">
{content[indexState]()}
</h2>
</>
);
}
This gets passed into as props.screens.
Modal.js
...
const screens = props.screens;
const Screens = (screens) => (<Screens index={{idx:0}}/>);
...
return (
{screens()} // works but doesn't let me pass in props
{Screens} // everything freezes
)
Really I just need a way to use Screens in Modal in a way that lets me dynamically send it props.
There is a lot of ways you could go about this, but here's just one:
// Modal.jsx
const Modal = ({ componentToRender }) => (
...JSX before component
{componentToRender()}
..JSX after component
)
// File where Modal is being rendered
const SomeComponent = () => <Modal componentToRender={() => <Screen id={1} />} />
There's a very large number of other ways to implement this same exact thing, you're more or less implementing a higher order component by the way you describe Modal.
Your original implementation in the second instance would work just fine, but you need to call Screens to get the returned component from the function:
...
const Screens = (screens) => (<Screens index={{idx:0}}/>);
...
return (
{Screens()}
)
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 use the function below (renderMatchedLogs) to render values from the object it receives, and I'm able to console.log the values but nothing displays on the screen.
I thought JSX can be rendered on the screen from another function? But I'm not sure if this something I misinterpreted or if my logic is off.
Further details of the code:
In the render() {} portion of the code:
<button onClick={this.findMatches}>Find Matches</button>
Which triggers this function to find matches:
findMatches = () => {
const foodLog = this.state.foodLog;
const foodFilter = this.state.foodFilter;
console.log("food filter", foodFilter);
Object.keys(foodLog).map((key, index) => {
if (foodLog[key].foodSelectedKey.some((r) => foodFilter.includes(r))) {
const matchedLog = foodLog[key];
this.renderMatchedLogs(matchedLog);
} else {
// do nothing
}
});
};
And then this is the function to render the values:
renderMatchedLogs = (matchedLog) => {
return (
<div>
{matchedLog.dateKey}
<br />
{matchedLog.mealKey}
<br />
{matchedLog.foodSelectedKey}
<br />
{matchedLog.reactionKey}
<br />
</div>
);
};
You’re rendering it, but not telling the application where to put it. I’d recommend putting the matchedLogs items in state somewhere that you update when you call findMatches, and then within your actual component have a something that looks like this
<div>
{matchedLogs && (renderMatchedLogs())}
<div>
Which can be the same as you have, apart from it’ll read the actual data from the state and render it rather than doing all of that itself (as I’m seeing from this context you want that to be user triggered).
May be what you're looking for is something like this?
state = {
logs: null
}
renderMatchedLogs = (matchedLog) => {
return (
<div>
{matchedLog.dateKey}
<br />
{matchedLog.mealKey}
<br />
{matchedLog.foodSelectedKey}
<br />
{matchedLog.reactionKey}
<br />
</div>
);
};
findMatches = () => {
const foodLog = this.state.foodLog;
const foodFilter = this.state.foodFilter;
console.log("food filter", foodFilter);
const matchedLogs = [];
//use forEach instead to push to matchedLogs variable
Object.keys(foodLog).forEach((key, index) => {
if (foodLog[key].foodSelectedKey.some((r) => foodFilter.includes(r))) {
const matchedLog = foodLog[key];
// this will return the div element from renderMatchedLogs
matchedLogs.push(this.renderMatchedLogs(matchedLog));
}
});
const logs = (<>
{matchedLogs.map(div => div)}
</>);
this.setState({
logs
})
};
render(){
return (
<>
{this.state.logs}
<button onClick={this.findMatches}>Find Matches</button>
</>
)
}
Can I pass the setPlaying as props? Or I have to do something like this example code?
Components Two and Three can be in their own file.
export const myComponent = () => {
const [playing, setPlaying] = useState(false);
const handleChange = (show) => {
setPlaying(show);
};
return (
<>
<One />
{!playing ? (
<Two handleChange={handleChange} />
) : (
<Three handleChange={handleChange} />
)}
</>
)
};
const Two = ({ handleChange }) => {
return (
<Container>
<Button onClick={{(e) => handleChange(true)}}>Click to Show Component Three</Button>
</Container>
);
};
const Three = ({ handleChange }) => {
return (
<Container>
<Button onClick={{(e) => handleChange(false)}}>Click to Show Component Two</Button>
</Container>
);
};
Of course. Wrapping setPlaying with handleChange is advantageous if you wish to do any further processing in myComponent. And the conditional rendering will prevent needless re-renders.
Yes, you can do that. Also, it is a good practice that you did with the handler.
ref: Passing setState to child component using React hooks
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>
)
}
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