How do I re-render react components on url change? - reactjs

I have a route like so:
<BrowserRouter>
<Switch>
<Route path="/question/:title" component={Item} />
</Switch>
</BrowserRouter>
The component uses axios to fetch data and update the content on the site, this is done in the componentWillMount function:
componentWillMount(){
const {title} = this.props.match.params;
axios.get(server + '/api/question/slug/' + title)
.then(response => {
this.setState({
question: response.data,
loading: false
})
})
}
The problem now is that, let's say I'm on a page "site.com/question/this-is-an-article",
and I use <Link to="/question/second-article">Second Article</Link> to navigate, it's like the componentWillMount function is not being called so the former content still remains on the page.
How can I make the componentWillMount function to run when there is a change in the url?

First of all, use safe life-cycle is preferred react-lifecycle-methods-diagram
Since you are using react-router-dom, you can get your URL via render props like this, refer to document here
history.location.pathname
So you may try to check if url changed in shouldComponentUpdate like this
shouldComponentUpdate(prevProps, prevState) {
return this.props.history.location.pathname !== prevProps.history.location.pathname
}
You can add more condition inside of it, or just prevent complex condition via component design adjustment

Try using the componentDidUpdate for the axios calling. Necessarily - you want to call the api when title(a prop) parameter change.
You may also try componentWillReceiveProps/UNSAFE_componentWillReceiveProps for the same.
P.S. - use componentDidMount along with above aptly. Look for lifecycle here http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

I think you don't need any life cycle, (if yes, just use componentDidMount):
class App extends Component {
constructor() {
super();
this._initHistoryListen();
}
_unListen = () => {};
_initHistoryListen() {
this._unListen = history.listen((location, action) => {
const {
title
} = this.props.match.params;
axios.get(server + '/api/question/slug/' + title)
.then(response => {
this.setState({
question: response.data,
loading: false
})
})
});
}
componentWillUnmount(){
this._unListen()
}
}

Related

Component did update works only after second click

My code adds a new item in the firebase databse when i click a button, then i want the list of objects in my page to automatically update, because i don't want to manualy reload the page. So i came up with this code
constructor(props){
super(props);
this.state = {
groups: [],
code:'',
name:'',
update:true
}
}
async fetchGroups (id){
fetchGroupsFirebase(id).then((res) => {this.setState({groups:res})})
};
async componentDidUpdate(prevProps,prevState){
if(this.state.update !== prevState.update){
await this.fetchGroups(this.props.user.id);
}
}
handleCreateSubmit = async event => {
event.preventDefault();
const{name} = this.state;
try{
firestore.collection("groups").add({
title:name,
owner:this.props.user.id
})
.then((ref) => {
firestore.collection("user-group").add({
idGroup:ref.id,
idUser:this.props.user.id
});
});
this.setState({update: !this.state.update});
}catch(error){
console.error(error);
}
What i was thinking, after i add the new item in firebase, i change the state.update variable, which triggers componentDidUpdate, which calls the new fetching.
I tried calling the fetchGroups function in the submit function, but that didn't work either.
What am i doing wrong and how could i fix it?
ComponentDidUpdate will not be called on initial render. You can either additionally use componentDidMount or replace the class component with a functional component and use the hook useEffect instead.
Regarding useEffect, this could be your effect:
useEffect(() => {
await this.fetchGroups(this.props.user.id);
}, [update]);
Since you can't use useEffect in class components so you would need to rewrite it as functional and replace your this.state with useState.

React-Router/Redux browser back button functionality

I'm building a 'Hacker News' clone, Live Example using React/Redux and can't get this final piece of functionality to work. I have my entire App.js wrapped in BrowserRouter, and I have withRouter imported into my components using window.history. I'm pushing my state into window.history.pushState(getState(), null, `/${getState().searchResponse.params}`) in my API call action creator. console.log(window.history.state) shows my entire application state in the console, so it's pushing in just fine. I guess. In my main component that renders the posts, I have
componentDidMount() {
window.onpopstate = function(event) {
window.history.go(event.state);
};
}
....I also tried window.history.back() and that didn't work
what happens when I press the back button is, the URL bar updates with the correct previous URL, but after a second, the page reloads to the main index URL(homepage). Anyone know how to fix this? I can't find any real documentation(or any other questions that are general and not specific to the OP's particular problem) that makes any sense for React/Redux and where to put the onpopstate or what to do insde of the onpopstate to get this to work correctly.
EDIT: Added more code below
Action Creator:
export const searchQuery = () => async (dispatch, getState) => {
(...)
if (noquery && sort === "date") {
// DATE WITH NO QUERY
const response = await algoliaSearch.get(
`/search_by_date?tags=story&numericFilters=created_at_i>${filter}&page=${page}`
);
dispatch({ type: "FETCH_POSTS", payload: response.data });
}
(...)
window.history.pushState(
getState(),
null,
`/${getState().searchResponse.params}`
);
console.log(window.history.state);
};
^^^ This logs all of my Redux state correctly to the console through window.history.state so I assume I'm implementing window.history.pushState() correctly.
PostList Component:
class PostList extends React.Component {
componentDidMount() {
window.onpopstate = () => {
window.history.back();
};
}
(...)
}
I tried changing window.history.back() to this.props.history.goBack() and didn't work. Does my code make sense? Am I fundamentally misunderstanding the History API?
withRouter HOC gives you history as a prop inside your component, so you don't use the one provided by the window.
You should be able to access the window.history even without using withRouter.
so it should be something like:
const { history } = this.props;
history.push() or history.goBack()

Return Fetched Data from Axios in React

Have a question about rendering fetched data with Axios. I’m able to log returned data to the console, however, it will not render on the screen.
I’m using an NPM Bitly module: https://www.npmjs.com/package/bitly
const BitlyClient = require('bitly');
const bitly = BitlyClient('ACCESS TOKEN');
State
class Example extends Component {
constructor() {
super();
this.state = {
landing: 'https://www.google.com/',
newUrl: 'https://www.udacity.com/'
};
API Call
componentDidMount() {
bitly.shorten(this.state.landing)
.then((response) => {
this.setState({newUrl: response.data.url })
console.log(response.data.url);
})
.catch(function(error) {
console.error(error);
});
}
This returns data to the console but does not render to the page.
Render to Page
<Component> {this.newUrl} </>
What am I missing?
It should be {this.state.newUrl}.
Literally started to work as soon as I posted this smh.
I just updated the component to include the state.
Did not work
<Component> {this.newUrl} </>
Does Work
<Component> {this.state.newUrl} </>

React Parent Component not re-rendering

I have a parent component that renders a list of children pulled in from an API (which functions correctly). Each child has an option to delete itself. When a child deletes itself, I cannot get the parent to re-render. I have read about 50 answers on here related to this topic and tried all of them and nothing seems to be working. I am missing something and stuck.
The component has redux wired in to it, but I have tried the code with and without redux wired up. I have also tried this.forceUpdate() in the callback, which also does not work (I've commented it out in the example code below).
class Parent extends React.Component {
constructor(props) {
super(props)
this.refresh = this.refresh.bind(this)
this.state = {
refresh: false,
}
}
componentDidMount(){
this.getChildren()
}
refresh = () => {
console.log("State: ", this.state)
this.setState({ refresh: !this.state.refresh })
// this.forceUpdate();
console.log("new state: ", this.state)
}
getChildren = () => {
axios.get(
config.api_url + `/api/children?page=${this.state.page}`,
{headers: {token: ls('token')}
}).then(resp => {
this.setState({
children: this.state.children.concat(resp.data.children)
)}
})
}
render(){
return (
<div>
{_.map(this.state.children, (chidlren,i) =>
<Children
key={i}
children={children}
refresh={() => this.refresh()}
/>
)}
</div>
)
}
}
And then in my Children component, which works perfectly fine, and the delete button successfully deletes the record from the database, I have the following excerpts:
deleteChild = (e) => {
e.preventDefault();
axios.delete(
config.api_url + `/api/children/${this.state.child.id}`,
{headers: {token: ls('token')}}
).then(resp => {
console.log("The response is: ", resp);
})
this.props.refresh();
}
render() {
return(
<button class="btn" onClick={this.deleteChild}>Delete</button>
)}
}
I am sure I am missing something simple or basic, but I can't find it.
Your parent render method depends only on this.state.children which is not changing in your delete event. Either pass in the child id to your this.props.refresh method like this.props.refresh(this.state.child.id) and update this.state.children inside the refresh method or call the get children method again once a delete happens
Code for delete method in child
this.props.refresh(this.state.child.id)
Code for parent refresh method
refresh = (childIdToBeDeleted) => {
console.log("State: ", this.state)
this.setState({ refresh: !this.state.refresh })
// this.forceUpdate();
console.log("new state: ", this.state)
//New code
this.setState({children: this.state.children.filter(child => child.id !== childIdToBeDeleted);
}
Few notes about the code. First removing from db and then reloading might be slow and not the best solution. Maybe consider adding remove() function which can be passed to the children component to update state more quickly.
Second if you want to call setState that depends on previous state it is better to use the callback method like this (but i think you need something else see below)
this.setState((prevState,prevProps) =>
{children: prevState.children.concat(resp.data.children)})
Lastly and what i think the issue is. You are not actually calling getChildren from refresh method so the state is not updated and if you want gonna reload the whole state from db you shouldn't concat but just set it like this
.then(resp => {
this.setState({children: resp.data.children})
}
Hope it helps.
Edit:
As mentioned in the comments the call to refresh from children should be in promise then

React-Router: how to wait for an async action before route transition

Is it possible to call an async redux action known as a thunk on a particular route and not perform the transition until the response has succeeded or failed?
Use Case
We need to load data from the server and fill a form with initial values. These initial values don't exist until the data is fetched from the server.
some syntax like this would be great:
<Route path="/myForm" component={App} async={dispatch(loadInitialFormValues(formId))}>
To answer the original question of preventing the transition to a new route until a response has succeeded or failed:
Because you're using redux thunk you could have the success or failure in the action creator trigger the redirect. I don't know what your specific action / action creator looks like but something like this could work:
import { browserHistory } from 'react-router'
export function loadInitialFormValues(formId) {
return function(dispatch) {
// hit the API with some function and return a promise:
loadInitialValuesReturnPromise(formId)
.then(response => {
// If request is good update state with fetched data
dispatch({ type: UPDATE_FORM_STATE, payload: response });
// - redirect to the your form
browserHistory.push('/myForm');
})
.catch(() => {
// If request is bad...
// do whatever you want here, or redirect
browserHistory.push('/myForm')
});
}
}
Follow up. Common pattern of loading data on entering a route / on componentWillMount of a component and displaying a spinner:
From the redux docs on async actions http://redux.js.org/docs/advanced/AsyncActions.html
An action informing the reducers that the request began.
The reducers may handle this action by toggling an isFetching flag in
the state. This way the UI knows it’s time to show a spinner.
An action informing the reducers that the request finished successfully.
The reducers may handle this action by merging the new data into the
state they manage and resetting isFetching. The UI would hide the
spinner, and display the fetched data.
An action informing the reducers that the request failed.
The reducers may handle this action by resetting isFetching.
Additionally, some reducers may want to store the error message so the
UI can display it.
I followed this general pattern below using your situation as a rough guideline. You do not have to use promises
// action creator:
export function fetchFormData(formId) {
return dispatch => {
// an action to signal the beginning of your request
// this is what eventually triggers the displaying of the spinner
dispatch({ type: FETCH_FORM_DATA_REQUEST })
// (axios is just a promise based HTTP library)
axios.get(`/formdata/${formId}`)
.then(formData => {
// on successful fetch, update your state with the new form data
// you can also turn these into their own action creators and dispatch the invoked function instead
dispatch({ type: actions.FETCH_FORM_DATA_SUCCESS, payload: formData })
})
.catch(error => {
// on error, do whatever is best for your use case
dispatch({ type: actions.FETCH_FORM_DATA_ERROR, payload: error })
})
}
}
// reducer
const INITIAL_STATE = {
formData: {},
error: {},
fetching: false
}
export default function(state = INITIAL_STATE, action) {
switch(action.type) {
case FETCH_FORM_DATA_REQUEST:
// when dispatch the 'request' action, toggle fetching to true
return Object.assign({}, state, { fetching: true })
case FETCH_FORM_DATA_SUCCESS:
return Object.assign({}, state, {
fetching: false,
formData: action.payload
})
case FETCH_FORM_DATA_ERROR:
return Object.assign({}, state, {
fetching: false,
error: action.payload
})
}
}
// route can look something like this to access the formId in the URL if you want
// I use this URL param in the component below but you can access this ID anyway you want:
<Route path="/myForm/:formId" component={SomeForm} />
// form component
class SomeForm extends Component {
componentWillMount() {
// get formId from route params
const formId = this.props.params.formId
this.props.fetchFormData(formId)
}
// in render just check if the fetching process is happening to know when to display the spinner
// this could also be abstracted out into another method and run like so: {this.showFormOrSpinner.call(this)}
render() {
return (
<div className="some-form">
{this.props.fetching ?
<img src="./assets/spinner.gif" alt="loading spinner" /> :
<FormComponent formData={this.props.formData} />
}
</div>
)
}
}
function mapStateToProps(state) {
return {
fetching: state.form.fetching,
formData: state.form.formData,
error: state.form.error
}
}
export default connect(mapStateToProps, { fetchFormData })(SomeForm)
I made a handy hook for this purpose, works with react-router v5:
/*
* Return truthy if you wish to block. Empty return or false will not block
*/
export const useBlock = func => {
const { block, push, location } = useHistory()
const lastLocation = useRef()
const funcRef = useRef()
funcRef.current = func
useEffect(() => {
if (location === lastLocation.current || !funcRef.current)
return
lastLocation.current = location
const unblock = block((location, action) => {
const doBlock = async () => {
if (!(await funcRef.current(location, action))) {
unblock()
push(location)
}
}
doBlock()
return false
})
}, [location, block, push])
}
Inside your component, use it like:
const MyComponent = () => {
useBlock(async location => await fetchShouldBlock(location))
return <span>Hello</span>
}
Navigation will not occur until the async function returns; you can completely block the navigation by returning true.
First and foremost, I want to say that there is a debate around the topic of fetching data with react-router's onEnter hooks whether or not is good practice, nevertheless this is how something like that would go:
You can pass the redux-store to your Router. Let the following be your Root component, where Router is mounted:
...
import routes from 'routes-location';
class Root extends React.Component {
render() {
const { store, history } = this.props;
return (
<Provider store={store}>
<Router history={history}>
{ routes(store) }
</Router>
</Provider>
);
}
}
...
And your routes will be something like:
import ...
...
const fetchData = (store) => {
return (nextState, transition, callback) => {
const { dispatch, getState } = store;
const { loaded } = getState().myCoolReduxStore;
// loaded is a key from my store that I put true when data has loaded
if (!loaded) {
// no data, dispatch action to get it
dispatch(getDataAction())
.then((data) => {
callback();
})
.catch((error) => {
// maybe it failed because of 403 forbitten, we can use tranition to redirect.
// what's in state will come as props to the component `/forbitten` will mount.
transition({
pathname: '/forbitten',
state: { error: error }
});
callback();
});
} else {
// we already have the data loaded, let router continue its transition to the route
callback();
}
}
};
export default (store) => {
return (
<Route path="/" component={App}>
<Route path="myPage" name="My Page" component={MyPage} onEnter={fetchData(store)} />
<Route path="forbitten" name="403" component={PageForbitten} />
<Route path="*" name="404" component={PageNotFound} />
</Route>
);
};
Please notice that your router file is exporting a thunk with your store as argument, if you look upwards, see how we invoked the router, we pass the store object to it.
Sadly, at the time of writing react-router docs return 404 to me, thus I cannot point you to the docs where (nextState, transition, callback) are described. But, about those, from my memory:
nextState describes the route react-router will transition to;
transition function to preform maybe another transition than the one from nextState;
callback will trigger your route transition to finish.
Another think to point out is that with redux-thunk, your dispatch action can return a promise, check it in the docs here. You can find here a good example on how to configure your redux store with redux-thunk.

Resources