React Link To not updating the component - javascript

I am using React with Redux to list number of items and inside the item I have a list of similar items
In Home Page (there is a list of items when you click on any of them , it goes to the item path ) which is working well , but inside the item page , when you click on any items from similar items list (the view not updating )
the codeSandobx is here
App.js
const store = createStore(ItemsReducer, applyMiddleware(...middlewares));
class App extends React.Component {
render() {
return (
<Provider store={store}>
<Main />
</Provider>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
main.js
const Main = () => {
return (
<Router>
<div>
<Header />
<div className="container-fluid">
<Switch>
<Route exact path="/" component={Home} />
<Route path="/item/:id" component={Item} />
</Switch>
</div>
</div>
</Router>
);
};
export default Main;
Home.js
class Home extends React.Component {
render() {
const itemsList = this.props.items.map(item => {
return <ItemList item={item} key={item.id} />;
});
return <div className="items-list"> {itemsList}</div>;
}
}
const mapStateToProps = state => ({
items: state.items,
user: state.user
});
export default connect(mapStateToProps, null, null, {
pure: false
})(Home);
Item.js
class Item extends React.Component {
constructor(props) {
super();
this.state = {
item_id: props.match.params.id,
};
}
render() {
const itemsList = this.props.items.map(item => {
return <ItemList item={item} key={item.id} />;
});
return (
<div id="item-container">
<div className="item-list fav-items"> {itemsList} </div>;
</div>
);
}
}
const mapStateToProps = state => ({
items: state.items,
user: state.user
});
export default connect(mapStateToProps, null, null, {
pure: false
})(Item);
and finally the ItemList.js
class ItemList extends React.Component {
render() {
const item = this.props.item;
const item_link = "/item/" + item.id;
return (
<Link to={item_link}>
<div className="item-li">
{item.title}
</div>
</Link>
);
}
}
export default ItemList;
I've tired to use this solution from react-redux docs , but it didn't work

What do you expect to update on link click?
Any path /item/:id (with any id: 2423, 2435, 5465) will show the same result, because you don't use params.id inside the Item component
UPDATED
When id changes the component doesn't remount, only updates component (It's correct behavior)
If you want to fetchData on each changes of id, the next solution has to work for you
on hooks:
const Item = () => {
const params = useParams();
useEffect(() => {
axios.get(`/item/${params.id}`).then(...)
}, [params.id]);
return (
...
)
}
useEffect will call fetch each time when id is changing
and in class component you have to use componentDidUpdate:
class Item extends Component {
componentDidMount() {
this.fetchData();
}
componentDidUpdate(prevProps) {
if (prevProps.match.params.id !== this.props.match.params.id) {
this.fetchData();
}
}
fetchData = () => {
...
}
...
}

Related

How to pass a function from Parent to an element within a route in React.js?

I want to pass a function to a component through a Route , I can do it with direct children but when it comes to Routes i can't figure it how to do it.
look at the code below , i want to pass "updateUserState" function to Profile Component , the function works properly in Header component but it's not working in Profile component which lives inside the Routes .
class App extends React.Component {
updateUserState = (currentUser) => {
if(currentUser != null) {
this.setState({
currentUser: currentUser
})
} else {
this.setState({
currentUser: null
});
}
return this.state.currentUser;
}
render() {
return (
<div className="App">
<Header updateUserState={this.updateUserState} />
<Routes>
<Route path='/profile' element={<ProfilePage updateUserState={this.updateUserState} />}/>
</Routes>
</div>
);
}
}
this is the code in Profile component which is completely similar to Header Component :
const ProfileCard = ({updateUserState}) => {
const signout = () => {
handleLogout()
updateUserState()
}
return (
<div className='profile-card'>
<a onClick={() => signout()}>
Sign Out
</a>
</div>
)
}
Update :
solved thanks to Abir Taheer !
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
currentUser: null
}
this.updateUserState = this.updateUserState.bind(this);
}
updateUserState = (currentUser) => {
if(currentUser != null) {
this.setState({
currentUser: currentUser
}, () => console.log(this.state.currentUser))
} else {
this.setState({
currentUser: null
}, () => console.log(this.state.currentUser));
}
return this.state.currentUser;
}
render() {
return (
<div className="App">
<Header currentUser={this.state.currentUser} updateUserState={this.updateUserState} />
<Routes>
<Route path='/profile' element={<ProfilePage updateUserState={this.updateUserState}
currentUser={this.state.currentUser} />}
/>
</Routes>
</div>
);
}
}
then inside ProfilePage :
const ProfilePage = ( {currentUser, updateUserState} ) => {
return (
<div>{
currentUser ?
<div>
<ProfileCard id={currentUser.id} updateUserState={updateUserState} />
</div>
:
<h1>No User Signed In</h1>
}</div>
)
}
And ProfileCard :
const ProfileCard = ({id, updateUserState}) => {
const signout = () => {
handleLogout()
updateUserState();
}
return (
<div className='profile-card'>
<a onClick={() => signout()}>
Sign Out
</a>
</div>
)
}
The issue arises because of the this keyword. When you're passing a function to another component you need to bind the this keyword to the parent instance otherwise it may not work properly.
This behavior is described in the React Docs here: https://reactjs.org/docs/faq-functions.html
and more specifically further down in the page here: Why is binding necessary at all?
When you bind this to the parent instance then it refers to the correct state and the function should work.
You need to update your component like such:
class App extends React.Component {
constructor(props) {
super(props);
// Make sure to initialize your state accordingly
this.state = {
currentUser: null,
};
// --- This is the line you need ---
this.updateUserState = this.updateUserState.bind(this);
}
updateUserState(currentUser) {
if (currentUser != null) {
this.setState({
currentUser: currentUser,
});
} else {
this.setState({
currentUser: null,
});
}
return this.state.currentUser;
}
render() {
return (
<div className="App">
<Header updateUserState={this.updateUserState} />
<Routes>
<Route
path="/profile"
element={<ProfilePage updateUserState={this.updateUserState} />}
/>
</Routes>
</div>
);
}
}
The way you do it seems like you are rendering the component instead of passing a reference.
How I would suggest is to wrap the component in another function and return with your function passed in as a prop. So basically making another react component with the method passed in. Use that wrapper component instead:
const wrappedProfilePage = () => <ProfilePage updateUserState={this.updateUserState} />;
..
.
.
<Route path='/profile' element={wrappedProfilePage}/>

Forward Ref giving value of current as null

I am trying to implement forward Ref in my Demo Project but I am facing one issue. The value of current coming from forward ref is null, but once I re-render my NavBar component (by sending a prop) I get the value of current.
I basically need to scroll down to my Section present in Home Component from NavBar Component.
It can be done by directly by giving a href attribute and passing the id. But I wanted to learn how forward ref works and hence this approach.
Can someone please help me with this?
Here is my Code.
import './App.css';
import NavBar from './components/NavBar/NavBar';
import Home from './components/Home/Home';
class App extends Component {
constructor(props) {
super(props);
this.homeRefService = React.createRef();
this.homeRefContact = React.createRef();
}
render() {
return (
<div className="App">
<NavBar name={this.state.name} homeRef={{homeRefService: this.homeRefService , homeRefContact: this.homeRefContact}}/>
<Home ref={{homeRefService: this.homeRefService, homeRefContact: this.homeRefContact }}/>
</div>
);
}
}
export default App;
**Home Component**
import React from 'react';
const home = React.forwardRef((props , ref) => {
const { homeRefService , homeRefContact } = ref;
console.log(ref);
return (
<div>
<section ref={homeRefService} id="Services">
Our Services
</section>
<section ref={homeRefContact} id="Contact">
Contact Us
</section>
</div>
)
})
export default home
**NavBar Component**
import React, { Component } from 'react'
export class NavBar extends Component {
render() {
let homeRefs = this.props.homeRef;
let homeRefServiceId;
let homeRefContactId;
if(homeRefs.homeRefService.current) {
homeRefServiceId = homeRefs.homeRefService.current.id;
}
if(homeRefs.homeRefContact.current ) {
homeRefContactId = homeRefs.homeRefContact.current.id;
}
return (
<div>
<a href={'#' + homeRefServiceId}> Our Services</a>
<a href={'#' + homeRefContactId }>Contact Us</a>
</div>
)
}
}
export default NavBar
The ref is only accessible when the component got mounted to the DOM. So you might want to access the DOM element in componentDidMount.I suggest you to lift the state up to the parent component.
Demo
// App
class App extends React.Component {
constructor(props) {
super(props);
this.homeRefService = React.createRef();
this.homeRefContact = React.createRef();
this.state = { homeServiceId: "", homeContactId: "" };
}
componentDidMount() {
this.setState({
homeServiceId: this.homeRefService.current.id,
homeContactId: this.homeRefContact.current.id
});
}
render() {
return (
<div className="App">
<NavBar
homeServiceId={this.state.homeServiceId}
homeContactId={this.state.homeContactId}
/>
<Home
ref={{
homeRefService: this.homeRefService,
homeRefContact: this.homeRefContact
}}
/>
</div>
);
}
}
// NavBar
export class NavBar extends Component {
render() {
return (
<div>
<a href={"#" + this.props.homeServiceId}> Our Services</a>
<a href={"#" + this.props.homeContactId}>Contact Us</a>
</div>
);
}
}
export default NavBar;
All your code just be oke. You can access ref after all rendered.
Example demo how do it work:
export class NavBar extends Component {
render() {
let homeRefs = this.props.homeRef;
console.log('from Nav Bar');
console.log(this.props.homeRef.homeRefService);
console.log('----');
let homeRefServiceId;
let homeRefContactId;
if(homeRefs.homeRefService.current) {
homeRefServiceId = homeRefs.homeRefService.current.id;
}
if(homeRefs.homeRefContact.current ) {
homeRefContactId = homeRefs.homeRefContact.current.id;
}
return (
<div>
<a href={'#' + homeRefServiceId}> Our Services</a>
<a href={'#' + homeRefContactId }>Contact Us</a>
</div>
)
}
}
const Home = React.forwardRef((props , ref) => {
const { homeRefService , homeRefContact } = ref;
useEffect(() => {
console.log('from home');
console.log(homeRefService);
console.log('----');
props.showUpdate();
})
return (
<div>
<section ref={homeRefService} id="Services">
Our Services
</section>
<section ref={homeRefContact} id="Contact">
Contact Us
</section>
</div>
)
})
class App extends Component {
state = {
name: 'init',
}
constructor(props) {
super(props);
this.homeRefService = React.createRef();
this.homeRefContact = React.createRef();
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('from app');
console.log(this.homeRefService);
console.log('----');
}
render() {
return (
<div className="App">
<div>{this.state.name}</div>
<NavBar name={this.state.name} homeRef={{homeRefService: this.homeRefService , homeRefContact: this.homeRefContact}}/>
<Home showUpdate={() => this.state.name === 'init' && setTimeout(() => this.setState({name: 'UpdatedRef'}), 2000)} ref={{homeRefService: this.homeRefService, homeRefContact: this.homeRefContact }}/>
</div>
);
}
}

REACT: Unable to update children props

I'm having troubles updating the header class so it updates it's className whenever displaySection() is called. I know that the parent state changes, because the console log done in displaySection() registers the this.state.headerVisible changes but nothing in my children component changes, i don't know what I'm missing, I've been trying different solutions for some hours and I just can't figure it out what i'm doing wrong, the header headerVisible value stays as TRUE instead of changing when the state changes.
I don't get any error code in the console, it's just that the prop headerVisible from the children Header doesn't get updated on it's parent state changes.
Thank you!
class IndexPage extends React.Component {
constructor(props) {
super(props)
this.state = {
section: "",
headerVisible: true,
}
this.displaySection = this.displaySection.bind(this)
}
displaySection(sectionSelected) {
this.setState({ section: sectionSelected }, () => {
this.sectionRef.current.changeSection(this.state.section)
})
setTimeout(() => {
this.setState({
headerVisible: !this.state.headerVisible,
})
}, 325)
setTimeout(()=>{
console.log('this.state', this.state)
},500)
}
render() {
return (
<Layout>
<Header selectSection={this.displaySection} headerVisible={this.state.headerVisible} />
</Layout>
)
}
}
const Header = props => (
<header className={props.headerVisible ? 'visible' : 'invisible'}>
<div className="navbar-item column is-size-7-mobile is-size-5-tablet is-uppercase has-text-weight-semibold">
<span onClick={() => { this.props.selectSection("projects")}}>
{" "}
Projects
</span>
</header>
)
There seemed to be a couple of issues with your example code:
Missing closing div in Header
Using this.props instead of props in onclick in span in Header
The below minimal example seems to work. I had to remove your call to this.sectionRef.current.changeSection(this.state.section) as I didn't know what sectionRef was supposed to be because it's not in your example.
class IndexPage extends React.Component {
constructor(props) {
super(props)
this.state = {
section: "",
headerVisible: true,
}
this.displaySection = this.displaySection.bind(this)
}
displaySection(sectionSelected) {
this.setState({ section: sectionSelected })
setTimeout(() => {
this.setState({
headerVisible: !this.state.headerVisible,
})
}, 325)
setTimeout(()=>{
console.log('this.state', this.state)
},500)
}
render() {
return (
<div>
<Header selectSection={this.displaySection} headerVisible={this.state.headerVisible} />
</div>
)
}
}
const Header = props => (
<header className={props.headerVisible ? 'visible' : 'invisible'}>
<div className="navbar-item column is-size-7-mobile is-size-5-tablet is-uppercase has-text-weight-semibold">
<span onClick={() => { props.selectSection("projects")}}>
{" "}
Projects
</span>
</div>
</header>
)
ReactDOM.render(
<IndexPage />,
document.getElementsByTagName('body')[0]
);
.visible {
opacity: 1
}
.invisible {
opacity: 0
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
There is a markup error in your code in Header component - div tag is not closed.
Also, I suppose, you remove some code to make example easy, and there is artifact of this.sectionRef.current.changeSection(this.state.section) cause this.sectionRef is not defined.
As #Felix Kling said, when you change the state of the component depending on the previous state use function prevState => ({key: !prevState.key})
Any way here is a working example of what you trying to achieve:
// #flow
import * as React from "react";
import Header from "./Header";
type
Properties = {};
type
State = {
section: string,
headerVisible: boolean,
};
class IndexPage extends React.Component<Properties, State> {
static defaultProps = {};
state = {};
constructor(props) {
super(props);
this.state = {
section: "",
headerVisible: true,
};
this.displaySection = this.displaySection.bind(this)
}
displaySection(sectionSelected) {
setTimeout(
() => this.setState(
prevState => ({
section: sectionSelected,
headerVisible: !prevState.headerVisible
}),
() => console.log("Debug log: \n", this.state)
),
325
);
}
render(): React.Node {
const {section, headerVisible} = this.state;
return (
<React.Fragment>
<Header selectSection={this.displaySection} headerVisible={headerVisible} />
<br/>
<div>{`IndexPage state: headerVisible - ${headerVisible} / section - ${section}`}</div>
</React.Fragment>
)
}
}
export default IndexPage;
and Header component
// #flow
import * as React from "react";
type Properties = {
headerVisible: boolean,
selectSection: (section: string) => void
};
const ComponentName = ({headerVisible, selectSection}: Properties): React.Node => {
const headerRef = React.useRef(null);
return (
<React.Fragment>
<header ref={headerRef} className={headerVisible ? 'visible' : 'invisible'}>
<div className="navbar-item column is-size-7-mobile is-size-5-tablet is-uppercase has-text-weight-semibold">
<span onClick={() => selectSection("projects")}>Projects</span>
</div>
</header>
<br/>
<div>Header class name: {headerRef.current && headerRef.current.className}</div>
</React.Fragment>
);
};
export default ComponentName;

React Context api - Consumer Does Not re-render after context changed

I searched for an answer but could not find any, so I am asking here,
I have a consumer that updates the context,
and another consumer that should display the context.
I am using react with typescript(16.3)
The Context(AppContext.tsx):
export interface AppContext {
jsonTransactions: WithdrawTransactionsElement | null;
setJsonTran(jsonTransactions: WithdrawTransactionsElement | null): void;
}
export const appContextInitialState : AppContext = {
jsonTransactions: null,
setJsonTran : (data: WithdrawTransactionsElement) => {
return appContextInitialState.jsonTransactions = data;
}
};
export const AppContext = React.createContext(appContextInitialState);
The Producer(App.tsx):
interface Props {}
class App extends React.Component<Props, AppContext> {
state: AppContext = appContextInitialState;
constructor(props : Props) {
super(props);
}
render() {
return (
<AppContext.Provider value={this.state}>
<div className="App">
<header className="App-header">
<SubmitTransactionFile/>
<WithdrawTransactionsTable />
</header>
</div>
</AppContext.Provider>
);
}
}
export default App;
The updating context consumer(SubmitTransactionFile.tsx)
class SubmitTransactionFile extends React.Component {
private fileLoadedEvent(file: React.ChangeEvent<HTMLInputElement>, context: AppContext): void{
let files = file.target.files;
let reader = new FileReader();
if (files && files[0]) {
reader.readAsText(files[0]);
reader.onload = (json) => {
if (json && json.target) {
// #ts-ignore -> this is because result field is not recognized by typescript compiler
context.setJsonTran(JSON.parse(json.target.result))
}
}
}
}
render() {
return (
<AppContext.Consumer>
{ context =>
<div className="SubmitTransactionFile">
<label>Select Transaction File</label><br />
<input type="file" id="file" onChange={(file) =>
this.fileLoadedEvent(file, context)} />
<p>{context.jsonTransactions}</p>
</div>
}
</AppContext.Consumer>
)
}
}
export default SubmitTransactionFile;
and finaly the display consumer(WithdrawTransactionsTable.tsx):
class WithdrawTransactionsTable extends React.Component {
render() {
return (
<AppContext.Consumer>
{ context =>
<div>
<label>{context.jsonTransactions}</label>
</div>
}
</AppContext.Consumer>
)
}
}
export default WithdrawTransactionsTable;
It is my understanding that after fileLoadedEvent function is called the context.setJsonTran should re-render the other consumers and WithdrawTransactionsTable component should be re-rendered , but it does not.
what am I doing wrong?
When you update the state, you aren't triggering a re-render of the Provider and hence the consumer data doesn't change. You should update the state using setState and assign context value to provider like
class App extends React.Component<Props, AppContext> {
constructor(props : Props) {
super(props);
this.state = {
jsonTransactions: null,
setJsonTran: this.setJsonTran
};
}
setJsonTran : (data: WithdrawTransactionsElement) => {
this.setState({
jsonTransactions: data
});
}
render() {
return (
<AppContext.Provider value={this.state}>
<div className="App">
<header className="App-header">
<SubmitTransactionFile/>
<WithdrawTransactionsTable />
</header>
</div>
</AppContext.Provider>
);
}
}
export default App;
Your setJsonTran just mutates the default value of the context which will not cause the value given to the Provider to change.
You could instead keep the jsonTransactions in the topmost state and pass down a function that will change this state and in turn update the value.
Example
const AppContext = React.createContext();
class App extends React.Component {
state = {
jsonTransactions: null
};
setJsonTran = data => {
this.setState({ jsonTransactions: data });
};
render() {
const context = this.state;
context.setJsonTran = this.setJsonTran;
return (
<AppContext.Provider value={context}>
<div className="App">
<header className="App-header">
<SubmitTransactionFile />
<WithdrawTransactionsTable />
</header>
</div>
</AppContext.Provider>
);
}
}
const AppContext = React.createContext();
class App extends React.Component {
state = {
jsonTransactions: null
};
setJsonTran = data => {
this.setState({ jsonTransactions: data });
};
render() {
const context = this.state;
context.setJsonTran = this.setJsonTran;
return (
<AppContext.Provider value={context}>
<div className="App">
<header className="App-header">
<SubmitTransactionFile />
<WithdrawTransactionsTable />
</header>
</div>
</AppContext.Provider>
);
}
}
class SubmitTransactionFile extends React.Component {
fileLoadedEvent(file, context) {
let files = file.target.files;
let reader = new FileReader();
if (files && files[0]) {
reader.readAsText(files[0]);
reader.onload = json => {
if (json && json.target) {
// slice just to not output too much in this example
context.setJsonTran(json.target.result.slice(0, 10));
}
};
}
}
render() {
return (
<AppContext.Consumer>
{context => (
<div className="SubmitTransactionFile">
<label>Select Transaction File</label>
<br />
<input
type="file"
id="file"
onChange={file => this.fileLoadedEvent(file, context)}
/>
<p>{context.jsonTransactions}</p>
</div>
)}
</AppContext.Consumer>
);
}
}
class WithdrawTransactionsTable extends React.Component {
render() {
return (
<AppContext.Consumer>
{context => (
<div>
<label>{context.jsonTransactions}</label>
</div>
)}
</AppContext.Consumer>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

object in redux state disappearing when another action is triggered

I have an array of data that is loaded into redux state when the Main component loads into the Data field, and I have a default app language of english also stored in redux state, if I click on my button to trigger the setLanguage action it will change the language but it will also empty the data array.
How can I prevent the data array from being emptied when I change the language??
redux
data: []
language: english
Main.js
class Main extends React.Component {
componentDidMount() {
this.props.fetchData()
}
render() {
const {language} = this.props
const e = language === 'english'
const p = language === 'polish'
return(
<Wrap>
<Router>
<ScrollToTop>
<Header />
<Wrap>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/reviews" component={Reviews} />
<button onClick={this.props.fetchData}>click</button>
{/* <Route exact path="/reviews/:catId" component={Reviews} />
<Route exact path="/reviews/:catId/:slug" component={Review} /> */}
{/* <Route exact path="/" component={Home} /> */}
{/* <ScrollToTop path="/reviews/:catId" component={Review} /> */}
{/* <ScrollToTop path="/another-page" component={Reviews} /> */}
</Switch>
</Wrap>
</ScrollToTop>
</Router>
</Wrap>
)
}
}
const mapStateToProps = state => ({
language: state.language
});
export default connect(mapStateToProps, actionCreators)(Main);
MainActions.js
import axios from 'axios'
import {
FETCH_DATA
} from '../../Constants'
export function fetchData() {
return dispatch =>
axios
.get('https://jsonplaceholder.typicode.com/users')
.then((response) => {
dispatch({ type: FETCH_DATA, payload: response.data });
})
.catch((err) => {
console.error(err);
});
}
dataReducer.js
import {
FETCH_DATA
} from '../Constants'
const dataReducer = (state = [], action) => {
return{
...state,
data: action.payload
}
}
export default dataReducer;
Header.js
class Header extends React.Component {
render() {
const {language} = this.props
const e = language === 'english'
const p = language === 'polish'
return (
<Wrapper>
<button onClick={()=>this.props.setLanguage('english')}>english</button>
<button onClick={()=>this.props.setLanguage('polish')}>polish</button>
<div>
{e && <div>language is english</div>}
{p && <div>language is polish</div>}
</Wrapper>
)
}
}
const mapStateToProps = state => ({
language: state.language
});
export default connect(mapStateToProps, actionCreators)(Header);
headerActions.js
import {
SET_LANGUAGE
} from '../../Constants'
export function setLanguage(language) {
return {
type: SET_LANGUAGE,
language
}
}
languageReducer.js
import {
SET_LANGUAGE
} from '../Constants'
const initialState = 'english'
const languageReducer = (state = initialState, action) => {
switch (action.type) {
case SET_LANGUAGE:
action.language
default:
return state;
}
};
export default languageReducer;
combineReducers.js
const rootReducer = combineReducers({
language: languageReducer,
data: dataReducer
});
export default rootReducer;
I have changed the dataReducer , it now stores the data and doesn't disappear when the SET_LANGUAGE action is triggered
const dataReducer = (state = [], action) => {
switch (action.type) {
case 'FETCH_DATA':
return {
...state,
data: action.payload
};
default:
return state;
}
}

Categories