I'm using withRouter() to transform a dropdown menu in a router.
Unfortunately, I need to use hashed routes.
Is there a similar approach that can be used with HashRouter.
Thank you.
UPDATE:
This is the relevant source in main.js.
class App extends React.Component {
// ...
render() {
return (
React.createElement(ReactRouterDOM.BrowserRouter, null,
React.createElement("div", null,
// ...
React.createElement(ReactRouterDOM.withRouter(Dropdown), {
_value: this.state.foo,
_options: [this.state.foo],
_onChangeCallback: this.handleChangeCallback // this updates the state
}),
// ...
)
)
)
}
}
In dropdown.js the component changes the route accordingly with the dropdown:
export default class Dropdown extends React.Component {
// ...
componentWillMount() {
if (this.props._value) this.props.history.push(`/${this.props._value}`); // update route
}
handleChange(event) {
this.props._onChangeCallback(event.target.value); // update the state
}
// ...
render() {
const options = this.props._options.map((val) => React.createElement("option", {value: val}, val));
return React.createElement("div", null,
React.createElement("span", null,
React.createElement("select", {
onChange: this.handleChange,
value: this.props._value,
}, options)));
}
}
When the component mounts there is an HTTP request to fetch some data;
The fetched data is saved into the state and the dropdown menu updates;
When the option changes the route changes as well.
Related
My React structure is
- App
|--SelectStudy
|--ParticipantsTable
In SelectStudy there is a button whose click triggers a message to its sibling, ParticipantsTable, via the App parent. The first Child->Parent transfer works. But how do I implement the second Parent->Child transfer? See questions in comments.
App
class App extends Component {
myCallback(dataFromChild) {
// This callback receives changes from SelectStudy Child Component's button click
// THIS WORKS
alert('SelectStudy Component sent value to Parent (App): ' + dataFromChild.label + " -> " + dataFromChild.value);
// QUESTION: How to Update State of ParticipantsTable (SelectStudy's Sibling) next?
// ........................................................
}
render() {
return (
<div className="App">
<SelectStudy callbackFromParent={this.myCallback}></SelectStudy>
<ParticipantsTable></ParticipantsTable>
</div>
);
}
SelectStudy
class SelectStudy extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: [],
selectedStudy: null,
isButtonLoading: false
};
this.handleButtonClick = this.handleButtonClick.bind(this);
}
render() {
const { error, isLoaded, items, itemsForReactSelect, selectedStudy, isButtonLoading } = this.state;
return <Button onClick={this.handleButtonClick}>Search</Button>;
}
handleButtonClick = () => {
this.props.callbackFromParent(this.state.selectedStudy);
}
}
ParticipantsTable - this needs to receive a certain variable, e.g. study in its State
class ParticipantsTable extends React.Component {
constructor(props) {
//alert('Constructor');
super(props);
// Initial Definition of this component's state
this.state = {
study: null,
items: [],
error: null
};
}
// THIS METHOD IS AVAILABLE, BUT HOW TO CALL IT FROM App's myCallback(dataFromChild)?
setStudy = (selectedStudy) => {
this.setState({study: selectedStudy});
}
render() {
return ( <div>{this.state.study}</div> );
}
}
The state should live definitively at the App level, not in the child. State needs to live one level above the lowest common denominator that needs access to it. So if both SelectStudy and ParticipantsTable need access to the same bit of state data, then it must live in their closest common ancestor (or above).
This is a core concept of React, known as "lifting state up", so much so that it has its own page in the official React documentation.
In your case, it would look something like this. Notice how state lives in only one place, at the <App /> level, and is passed to children via props.
import React from 'react';
class App extends React.Component {
// State lives here at the closest common ancestor of children that need it
state = {
error: null,
isLoaded: false,
items: [],
selectedStudy: null,
isButtonLoading: false
};
myCallback = (dataFromChild) => {
this.setState(dataFromChild);
};
render() {
return (
<div className="App">
{/* State is passed into child components here, as props */}
<SelectStudy data={this.state} callbackFromParent={this.myCallback}></SelectStudy>
<ParticipantsTable study={this.state.selectedStudy} />
</div>
);
}
}
class SelectStudy extends React.Component {
handleButtonClick = () => {
// Here we execute a callback, provided by <App />, to update state one level up
this.props.callbackFromParent({ ...this.props.selectedStudy, isButtonLoading: true });
};
render() {
const { error, isLoaded, items, itemsForReactSelect, selectedStudy, isButtonLoading } = this.props.data;
return <Button onClick={this.handleButtonClick}>Search</Button>;
}
}
// This component doesn't need to track any internal state - it only renders what is given via props
class ParticipantsTable extends React.Component {
render() {
return <div>{this.props.study}</div>;
}
}
I think what you need to understand is the difference between state and props.
state is internal to a component while props are passed down from parents to children
Here is a in-depth answer
So you want to set a state in the parent that you can pass as props to children
1 set state in the parent
this.state = {
value: null
}
myCallback(dataFromChild) {
this.setState({value: dataFromChild.value})
}
2 pass it as a prop to the children
class ParticipantsTable extends React.Component {
constructor(props) {
super(props);
this.state = {
study: props.study,
items: [],
error: null
};
}
Also, although not related to your question, if you learning React I suggest moving away from class-based components in favour of hooks and functional components as they have become more widely used and popular recently.
I have set a context which has fields updated by an API call:
export const CharacterContext = React.createContext()
export class CharacterProvider extends Component {
constructor(props) {
super(props);
this.state = {
text: "test, test",
name: "no-name",
id: "999",
currStamina: 0,
abilities: {
"bra": 0,
"agi": 0,
"int": 0,
"cun": 0,
"will": 0,
"pre": 0
},
skills: [],
characters: [],
getCharacter: this.getCharacter,
}
}
getCharacter = (id) => {
CharacterDataService.getCharacterById(id)
.then(
response => {
console.log(response);
this.setState({
name: response.data.username,
maxStamina: response.data.stamina,
currStamina: response.data.stamina,
id: response.data.id,
abilities: response.data.abilities,
skills: response.data.skills
});
}
);
}
render() {
return (
<CharacterContext.Provider
value={this.state}>
{
this.props.children
}
</CharacterContext.Provider>
)
}
}
export const CharacterConsumer = CharacterContext.Consumer
Another element has set Provider and Consumer inside for the purpose of testing. It receives the state from the provider, but even though the element is rendered AFTER context update (through a Router), it shows original values (abilities set to 0 etc...).
export default class Edit extends React.Component {
static contextType = CharacterContext;
constructor(props) {
super(props);
}
render() {
return (
<CharacterProvider>
<CharacterConsumer>
{({text, currStamina}) => (
<p>{text} : {currStamina}</p>
)}
</CharacterConsumer>
<Abilities/>
</CharacterProvider>
)
}
}
What am I missing? Why does the values in context update by the API call but the consumer element still shows the original values?
In my opinion it's a router problem.
Can you check if you <Link /> components are rendered outside of the router context. If is rendered outside the Router, meaning the Links will fallback to the default value passed to createContext.
For some reason the problem was in nesting consumer element directly under the provider. What didn't work:
render() {
return (
<CharacterProvider>
<CharacterConsumer>
{({text, currStamina}) => (
<p>{text} : {currStamina}</p>
)}
</CharacterConsumer>
<Abilities/>
</CharacterProvider>
)
}
This didn't work (change in the Provider didn't propagate to the consumer). As soon as I moved the Provider to parent element, the consumer was update with every change of the provider.
render() {
return (
<div>
<CharacterConsumer>
{({text, currStamina}) => (
<p>{text} : {currStamina}</p>
)}
</CharacterConsumer>
</div>
)
}
I have an app with redux and router where on the first load, all users are loaded. To this end, I've implemented a main component that loads the user when the component is mounted:
class Content extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.load();
}
render() {
return this.props.children;
}
}
The afterwards, if the user chooses to load the details of one user, the details are also obtained through the same lifehook:
class Details extends Component {
componentDidMount() {
this.props.getByUrl(this.props.match.params.url);
}
render() {
const { user: userObject } = this.props;
const { user } = userObject;
if (user) {
return (
<>
<Link to="/">Go back</Link>
<h1>{user.name}</h1>
</>
);
}
return (
<>
<Link to="/">Go back</Link>
<div>Fetching...</div>
</>
);
}
Now this works well if the user lands on the main page. However, if you get directly to the link (i.e. https://43r1592l0.codesandbox.io/gal-gadot) it doesn't because the users aren't loaded yet.
I made a simple example to demonstrate my issues. https://codesandbox.io/s/43r1592l0 if you click a link, it works. If you get directly to the link (https://43r1592l0.codesandbox.io/gal-gadot) it doesn't.
How would I solve this issue?
Summary of our chat on reactiflux:
To answer your question: how would you solve this? -> High Order Components
your question comes down to "re-using the fetching all users before loading a component" part.
Let's say you want to show a Component after your users are loaded, otherwise you show the loading div: (Simple version)
import {connect} from 'react-redux'
const withUser = connect(
state => ({
users: state.users // <-- change this to get the users from the state
}),
dispatch => ({
loadUsers: () => dispatch({type: 'LOAD_USERS'}) // <-- change this to the proper dispatch
})
)
now you can re-use withUsers for both your components, which will look like:
class Content extends Component {
componentDidMount() {
if (! this.props.users || ! this.props.users.length) {
this.props.loadUsers()
}
}
// ... etc
}
const ContentWithUsers = withUsers(Content) // <-- you will use that class
class Details extends Component {
componentDidMount() {
if (! this.props.users || ! this.props.users.length) {
this.props.loadUsers()
}
}
}
const DetailsWithUsers = withUsers(Details) // <-- same thing applies
we now created a re-usable HOC from connect. you can wrap your components with withUsers and you can then re-use it but as you can see, you are also re-writing the componentDidMount() part twice
let's take the actual load if we haven't loaded it part out of your Component and put it in a wrapper
const withUsers = WrappedComponent => { // notice the WrappedComponent
class WithUsersHOC extends Component {
componentDidMount () {
if (!this.props.users || !this.props.users.length) {
this.props.loadUsers()
}
}
render () {
if (! this.props.users) { // let's show a simple loading div while we haven't loaded yet
return (<div>Loading...</div>)
}
return (<WrappedComponent {...this.props} />) // We render the actual component here
}
}
// the connect from the "simple version" re-used
return connect(
state => ({
users: state.users
}),
dispatch => ({
loadUsers: () => dispatch({ type: 'LOAD_USERS' })
})
)(WithUsersHOC)
}
Now you can just do:
class Content extends Component {
render() {
// ......
}
}
const ContentWithUsers = withUsers(Content)
No need to implement loading the users anymore, since WithUsersHOC takes care of that
You can now wrap both Content and Details with the same HOC (High Order Component)
Until the Users are loaded, it won't show the actual component yet.
Once the users are loaded, your components render correctly.
Need another page where you need to load the users before displaying? Wrap it in your HOC as well
now, one more thing to inspire a bit more re-usability
What if you don't want your withLoading component to just be able to handle the users?
const withLoading = compareFunction = Component =>
class extends React.Component {
render() {
if (! compareFunction(this.props)) {
return <Component {...this.props} />;
}
else return <div>Loading...</div>;
}
};
now you can re-use it:
const withUsersLoading = withLoading(props => !props.users || ! props.users.length)
const ContentWithUsersAndLoading = withUsers(withUsersLoading(Content)) // sorry for the long name
or, written as a bit more clean compose:
export default compose(
withUsers,
withLoading(props => !props.users || !props.users.length)
)(Content)
now you have both withUsers and withLoading reusable throughout your app
I'm running into a problem getting a child react component to update when its parent stage changes. I have an Editor parent component that sets its state and then updates the state if the component receives an updated schedule (from a graphQL mutation component).
The problem is that componentDidUpdate triggers which does trigger the Modefield to update, but it is before the setState in componentDidUpdate can update the state. This means the child doesn't update. (Note- I know a more idiomatic way is to get rid of state all together, but this way allows a field to both edit and create a new one.)
How can I cause the child to update based on the parent's state change?
export const updateScheduleMutation = gql`
mutation updateScheduleMutation(
$id: ID!
$mode: String
) {
updateSchedule(
id: $id
mode: $mode
) {
id
mode
}
}
`;
class EditorWrapper extends React.Component {
constructor(props) {
super(props);
this.state = { scheduleId: props.scheduleId || '' };
}
render() {
return (
<Mutation mutation={updateScheduleMutation}>
{(updateSchedule, { mutationData }) => <Editor {...data} updateSchedule={updateSchedule} />}
</Mutation>
)
}
}
class Editor extends React.Component {
constructor(props) {
super(props);
const { schedule } = props;
if(schedule === null){
this.state = {
schedule: { mode: schedule.mode || "" }
};
}
}
componentDidUpdate(prevProps) {
if (prevProps.schedule !== this.props.schedule) {
this.setState({ ...this.props.schedule });
}
}
changeInput = (path, input) => {
const { updateSchedule, schedule } = this.props;
const field = path.split('.')[1];
updateSchedule({ variables: { id: schedule.id, [field]: input } });
this.setState({ [path]: input });
};
render() {
return (
<ModeField input={this.state.schedule.input} />
);
}
}
const ModeField = ({input}) => FormControl value={input} />
EDIT: I updated the component to show the higher level graphQL wrapper. The reason why I wanted state in the Editor component is that in the event the graphQL query comes back as null, I set this.state.mode to an empty string, which I then update on change. Then, I would create a new schedule item on submit.
LIFT THE STATE UP! Try to manage the base state of your data in parent component and use the data as props in your component:
You also can try getDerivedStateFromProps, but before check the react blog advices:
https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html
I stuck in bit weird situation, I am using ReactJS. I have header container, title bar, title container. Header container has navigation bar. On click of that it effects title bar. I am using react router for that navigation. I am using componentDidMount lifecycle method for that.
Problem with that it triggers only once when title container loads. So I used componentDidUpdate. But in that problem occured when I added title bar component to title container. So now my componentDidUpdate runing in infinite loop. I tried to use shouldComponentUpdate(nextProps, nextState) but I don't know what condition put to return it false.
export class TitleContainer extends React.Component {
componentDidMount() {
this.props.dispatch(fetchDetail(this.props.match.params.program_id))
}
componentDidUpdate(prevProps, prevState) {
this.props.dispatch(fetchDetail(this.props.match.params.id))
}
shouldComponentUpdate(nextProps, nextState){
console.log("current props",this.props)
console.log("next props",nextProps)
// if(this.props.name == nextProps.name)
// return false;
return true;
}
render() {
console.log("data in contaner", this.props)
return (
<div>
<Title name = { this.props.name }
/>
</div>
)
}
}
const mapStateToProps = (state) => {
console.log("update state", state)
return {
programProfileData: state.DetailReducer.Details,
name: state.DetailReducer.name
}
}
export default connect(mapStateToProps)(TitleContainer)
If I understand your problem you would like to fetch other data if you change the params?
If so I would just remount the whole component.
class TitleContainer extends React.Component {
componentDidMount() {
this.props.dispatch(fetchDetail(this.props.match.params.program_id))
}
render() {
console.log("data in contaner", this.props)
return (
<div>
<Title name = { this.props.name }
/>
</div>
)
}
}
const mapStateToProps = (state) => {
console.log("update state", state)
return {
programProfileData: state.DetailReducer.Details,
name: state.DetailReducer.name
}
}
export default connect(mapStateToProps)(TitleContainer)
I think you don't need to use componentUpdate. You navigate to your that address, react router will creates that component, and you can extract the match props.
In your header you can have other Links from the react router dom lib which will replace your existing component. If you click on a link, react router pushes that to the browser history and creates a new component and therefore the params are updated.