The Blueprint UI library provides a Toaster component that displays a notice for a user's action. From the documentation, it's used by first calling
const MyToaster = Toaster.create({options}), followed by
MyToaster.show({message: 'some message'}).
I'm having trouble fitting the show method into React's lifecycle - how do I create a reusable toaster component that will display different messages on different button clicks? If it helps, I am using MobX as a data store.
The Toaster is a funny one in this regard because it doesn't care about your React lifecycle. It's meant to be used imperatively to fire off toasts immediately in response to events.
Simply call toaster.show() in the relevant event handler (whether it's a DOM click or a Redux action).
See how we do it in the example itself: toastExample.tsx
My solution with Redux (and redux-actions)...
Action:
export const setToaster = createAction('SET_TOASTER');
Reducer:
const initialState = {
toaster: {
message: ''
}
};
function reducer(state = initialState, action) {
switch(action.type) {
case 'SET_TOASTER':
return {
...state,
toaster: { ...state.toaster, ...action.payload }
};
};
}
Toaster Component:
// not showing imports here...
class MyToaster extends Component {
constructor(props) {
super(props);
this.state = {
// set default toaster props here like intent, position, etc.
// or pass them in as props from redux state
message: '',
show: false
};
}
componentDidMount() {
this.toaster = Toaster.create(this.state);
}
componentWillReceiveProps(nextProps) {
// if we receive a new message prop
// and the toaster isn't visible then show it
if (nextProps.message && !this.state.show) {
this.setState({ show: true });
}
}
componentDidUpdate() {
if (this.state.show) {
this.showToaster();
}
}
resetToaster = () => {
this.setState({ show: false });
// call redux action to set message to empty
this.props.setToaster({ message: '' });
}
showToaster = () => {
const options = { ...this.state, ...this.props };
if (this.toaster) {
this.resetToaster();
this.toaster.show(options);
}
}
render() {
// this component never renders anything
return null;
}
}
App Component:
Or whatever your root level component is...
const App = (props) =>
<div>
<MyToaster {...props.toaster} setToaster={props.actions.setToaster} />
</div>
Some Other Component:
Where you need to invoke the toaster...
class MyOtherComponent extends Component {
handleSomething = () => {
// We need to show a toaster!
this.props.actions.setToaster({
message: 'Hello World!'
});
}
render() { ... }
}
Related
This is the response from redux store :
{
"newsletter": true,
"orderConfirmation": true,
"shippingInformation": true,
"orderEnquiryConfirmation": true,
}
This is the jsx file, where am trying to set state. The idea is setting the state from the response and add an onChange handle to each checkboxes.
But currently am receiving a correct response but I tried to set state in didUpdate, DidMount but no luck. I want to know the correct place to set state on initial render of the component.
import React from 'react';
import Component from '../../assets/js/app/component.jsx';
import { connect } from 'react-redux';
import * as actionCreators from '../../assets/js/app/some/actions';
import { bindActionCreators } from 'redux';
import Checkbox from '../checkbox/checkbox.jsx';
const mapStateToProps = (state, ownProps) => {
return {
...state.emailSubscriptions
}
}
const mapDispatchToProps = dispatch => {
return {
actions: bindActionCreators(actionCreators, dispatch)
}
}
#connect(mapStateToProps, mapDispatchToProps)
class EmailSubscriptions extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
this.props.actions.getEmailSubscriptions();
this.setState({ // Not setting state
notifications: [
newsletter = this.props.newsletter,
orderConfirmation = this.props.orderConfirmation,
shippingInformation = this.props.shippingInformation,
orderEnquiryConfirmation = this.props.orderEnquiryConfirmation
]
})
}
render() {
return (
<div>
Here I want to use loop through state to create checkboxes
{this.state.notifications&& this.state.notifications.map((item, index) => {
const checkboxProps = {
id: 'subscription' + index,
name: 'subscription',
checked: item.subscription ? true : false,
onChange: (e)=>{ return this.onChange(e, index)},
};
return <div key={index}>
<Checkbox {...checkboxProps} />
</div>
</div>
)
}
}
export default EmailSubscriptions;
I hope getEmailSubscriptions is an async action, so your setState won't update the state as you intended. add componentDidUpdate hook in your class component and your setState statement within an if statement that has an expression checking your props current and prev value.
You can do something like this.
componentDidMount() {
this.props.actions.getEmailSubscriptions();
}
componentDidUpdate(prevProps, prevState, snapshot){
if(this.props.<prop_name> != prevProps.<prop_name>){
this.setState({
notifications: [
newsletter = this.props.newsletter,
orderConfirmation = this.props.orderConfirmation,
shippingInformation = this.props.shippingInformation,
orderEnquiryConfirmation = this.props.orderEnquiryConfirmation
]
})
}
}
React & Firebase newbie here. I have a React component that needs to look up some stuff in Firebase before rendering. My database design requires first getting the correct doohick ids and subsequently looking up the doohick details, but I'm not sure how to do that with the asynchronous nature of Firebase database access. This doesn't work:
class Widget extends React.Component {
componentDidMount() {
firebase.database().ref(`/users/${username}/doohick-ids`).on('value', snapshot => {
this.setState({doohick_ids: doohick_ids});
});
this.state.doohick_ids.forEach(id => {
// ids don't actually exist at this point outside the callback
firebase.database().ref(`/doohick-details/${id}`).on('value', snapshot => {
// update state
});
});
render() {
if (this.state.doohick-ids) {
return null;
} else {
// render the Doohick subcomponents
}
}
}
I can think of a few solutions here, but none that I like. What's the recommended way to chain together Firebase calls, or perhaps redesign this to eliminate the problem?
I think you should split one component Widget to two WidgetList and WidgetItem.
WidgetItem
subscribe and unsubscribe to firebase.database().ref(/doohick-details/${id})
class WidgetItem extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
}
constructor(props) {
super(props);
this.state = {};
this.dbRef = null;
this.onValueChange = this.onValueChange.bind(this);
}
componentDidMount() {
const { id } = this.props;
this.dbRef = firebase.database().ref(`/doohick-details/${id}`);
this.dbRef.on('value', this.onValueChange);
}
componentWillUnmount() {
this.dbRef.off('value', this.onValueChange);
}
onValueChange(dataSnapshot) {
// update state
this.setState(dataSnapshot);
}
render() {
return (
<pre>{JSON.stringify(this.state, null, 2)}</pre>
);
}
}
WidgetList
subscribe and unsubscribe to firebase.database().ref(/users/${username}/doohick-ids)
class WidgetItem extends React.Component {
constructor(props) {
super(props);
this.state = { doohick_ids: [] };
this.dbRef = null;
this.onValueChange = this.onValueChange.bind(this);
}
componentDidMount() {
// Note: I've just copied your example. `username` is undefined.
this.dbRef = firebase.database().ref(`/users/${username}/doohick-ids`);
this.dbRef.on('value', this.onValueChange);
}
componentWillUnmount() {
this.dbRef.off('value', this.onValueChange);
}
onValueChange(dataSnapshot) {
this.setState({ doohick_ids: dataSnapshot });
}
render() {
const { doohick_ids } = this.state;
if (doohick_ids.length === 0) {
return 'Loading...';
}
return (
<React.Fragment>
{doohick_ids.map(id => <WidgetItem key={id} id={id} />)}
</React.Fragment>
);
}
}
And code that requires the data from the database needs to be inside the callback that is invoked when that data is available. Code outside of the callback is not going to have the right data.
So:
firebase.database().ref(`/users/${username}/doohick-ids`).on('value', snapshot => {
this.setState({doohick_ids: doohick_ids});
doohick_ids.forEach(id => {
// ids don't actually exist at this point outside the callback
firebase.database().ref(`/doohick-details/${id}`).on('value', snapshot => {
// update state
});
});
});
There's many optimizations possible here, but they all boil down to the code being inside the callback and updating the state when a value comes from the database.
I'm working on an environment that is basically set up with a Main Component like this:
class MainComponent extends Component {
constructor(props) {
super(props);
this.state = {
selectedValues: []
};
}
render() {
const { selectedValues } = this.state;
return (
// Other components
<SubComponent selectedValues = {selectedValues} />
// Other components
);
}
}
export default MainComponent;
And a Sub Component like this:
class SubComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
isExporting: false,
selectedValues: props.selectedValues
};
}
performTask = () => {
this.setState({ isWorking: true });
const { selectedValues } = this.state;
console.log(`Selected Values: ${selectedValues}`);
fetch('/api/work', {
method: 'GET'
})
.then(res => res.json())
.then((result) => {
// Handle the result
this.setState({ isWorking: false });
})
.catch((error) => {
console.log(error);
this.setState({ isWorking: false });
});
};
render() {
const { isWorking } = this.state;
return (
<Button
bsStyle="primary"
disabled={isWorking}
onClick={() => this.performTask()}
>
{isWorking ? 'Working...' : 'Work'}
</Button>
);
}
}
SubComponent.propTypes = {
selectedValues: PropTypes.arrayOf(PropTypes.string)
};
SubComponent.defaultProps = {
selectedValues: []
};
export default SubComponent;
In the Main Component, there are other components at work that can change the selectedValues. The functionality I'd like to see is that when the performTask method fires, it has the most recent and up to date list of selectedValues. With my current setup, selectedValues is always an empty list. No matter how many values actually get selected in the Main Component, the list never seems to change in the Sub Component.
Is there a simple way to do this?
I would suggest you 2 of the following methods to check this problem:
Maybe the state.selectedItems doesn't change at all. You only declare it in the contractor but the value remains, since you didn't setState with other value to it. Maybe it will work if you will refer to this.props.selectedItems instead.
Try to add the function component WillReceiveProps(newProps) to the sub component and check the value there.
If this method doesn't call, it means the selectedItems doesnt change.
Update if some of it works.
Good luck.
selectedValues in SubComponent state has not updated since it was set in SubComponent constructor. You may need to call setState again in componentWillReceivedProps in SubComponent
i'm rendering "Details" component in a callback in my UsersListContainer like this:
class UsersListContainer extends Component {
goToUserById(id) {
if (!id) { return false; }
this.props.history.push(`/users/${id}`);
}
render() {
return (
<UserList
goToUser={(id) => this.goToUserById(id)}/>
);
}
}
My "Details" container:
class UserDetailsContainer extends Component {
componentDidMount() {
this.props.getUserDetails(this.props.match.params.id);
}
render() {
return (
<UserDetails user={this.props.selectedUser}/>
);
}
}
const mapDispatchToProps = dispatch => {
return {
getUserDetails: id => dispatch(getUser(id))
};
};
const mapStateToProps = (state) => ({
selectedUser: state.user.selectedUser
});
And in my presentational "User" component I display a set of data from redux store like this:
class UserDetails extends Component {
constructor(props) {
super(props);
this.state = {
name: this.props.user.name,
address: this.props.user.name,
workingHours: this.props.user.workingHours,
phone: this.props.user.phone
};
}
I'm not displaying component props directly and I use state because they are meant to be edited. This works, but the problem is that all these props are not updating simultaneously with component load which means when I select user for the first time it displays the right info, then I switch back to "/users" to choose another, and his props remain the same as props of the previous user. I tried componentWillUnmount to clear the data but it didn't work
Solved this by using lodash lib
in my presentational component I compare if objects are equal
constructor(props) {
super(props);
this.state = {
name: "",
address: ""
};
}
componentWillReceiveProps(nextProps) {
_.isEqual(this.props.user, nextProps.user) ? (
this.setState({
name: this.props.user.name,
address: this.props.user.address
})
) : (
this.setState({
name: nextProps.user.name,
address: nextProps.user.address,
})
)
}
When you implement a Route like /users/:id, and if you change the id to something else, the entire component is not re-mounted and hence the componentDidMount is not called, rather only the props change and hence you need to implement componentWillReceiveProps function also
componentDidMount() {
this.props.getUserDetails(this.props.match.params.id);
}
componentWillReceiveProps(nextProps) {
if(this.props.match.params.id !== nextProps.match.params.id) {
this.props.getUserDetails(nextProps.match.params.id);
}
}
I have a simple component consisting of an input (jobId) and a span (jobDescription). When the user enters a jobId, the component makes an ajax call to fill in the description. Sounds easy. But here is my problem, the way I wrote it, the component makes a bunch of wasteful calls to lookup the jobDescription with each keypress.
I tried changing the lookup to onBlur instead of onChange, but then I miss the "initial load" (the initial render when the jobId is passed in from the parent). So jobDescription is blank when the form is first opened.
So here is what I need:
When the user types in a jobId, lookup the corresponding jobDescription. But not necessarily on every key stroke.
When the parent passes in a jobId, it should also lookup the corresponding jobDescription.
Here is the component as it is now:
import React from "react";
type Props = {
jobId:string,
onChange:(jobId: string)=>void
}
type State = {
description:string
}
export default class JobComponent extends React.Component {
props: Props;
state: State;
constructor(props: Props) {
super(props);
this.state = {
description: ""
}
}
componentWillReceiveProps(nextProps: Props) {
if (nextProps.jobId != this.props.jobId) {
this.loadDescription(nextProps.jobId)
.then((description)=> {
this.setState({description});
});
}
}
onChangeInternal = (event)=> {
const jobId = event.target.value;
this.props.onChange(jobId);
this.loadDescription(jobId)
.then((description)=> {
this.setState({description});
});
};
render() {
return <div>
<input className="job-id" value={this.props.jobId} onChange={this.onChangeInternal}/>
<span className="job-description">{this.state.description}</span>
</div>;
}
loadDescription = (jobId): Promise<string> => {
return new Promise((resolve) => {
if (!jobId) resolve("");
else
fetch('/components/job-picker/jobService.jsp?jobId=' + jobId)
.then((response) => {
return response.json();
})
.then((job) => {
resolve(job.description);
});
});
};
}
And here is a sample for the parent component:
import React from "react";
import JobComponent from "./JobComponent";
export default class FormTest extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "Dave",
jobId: "360107",
age: 50
}
}
render() {
const onNameChange = (event)=> {
this.setState({name: event.target.value});
};
const onAgeChange = (event)=> {
this.setState({age: event.target.value});
};
const onJobIdChange = (jobId)=> {
this.setState({jobId});
};
return (
<div>
Name<input value={this.state.name} onChange={onNameChange}/><br/>
JobId<JobComponent jobId={this.state.jobId} onChange={onJobIdChange}/><br/>
Age<input value={this.state.age} onChange={onAgeChange}/><br/>
</div>
);
}
}
So, assuming who understand what I'm trying to do, how would you write this component in React?
You should wait for a particular time before making a request. This is primarily delaying the request and wait for user input. Change your onChange handler to something like this
onChangeInternal = (event)=> {
const jobId = event.target.value;
this.props.onChange(jobId);
clearTimeout(this.timeoutId); // resets the timeout on new input
this.timeoutId = setTimeout(() => {
this.loadDescription(jobId)
.then((description)=> {
this.setState({description});
});
}, 500); //delays request by 500ms.
};
You should also check Lodash's Debounce. This will delay the function call for a given time.
Hope this helps!