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>
Related
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>
);
}
}
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 = () => {
...
}
...
}
New to and learning React. I have a data file that I am reading in in order to render the Card component for each item. Right now, just one card with nothing in it (one card in the initial state) renders. How do I render multiple components by passing through properties from a data file?
Card.js
import React from 'react';
import * as d3 from "d3";
import data from './../data/data.csv';
class Card extends React.Component {
constructor(){
super();
this.state={
text:[],
}
}
componentDidMount() {
d3.csv(data)
.then(function(data){
console.log(data)
let text = data.forEach((item)=>{
console.log(item)
return(
<div key={item.key}>
<h1>{item.quote}</h1>
</div>
)
})
this.setState({text:text});
console.log(this.state.text);
})
.catch(function(error){
})
}
render() {
return(
<div className='card'>
{this.state.text}
</div>
)
}
}
export default Card
index.js
import Card from './components/Card'
ReactDOM.render(<Card />, document.getElementById('root'));
Answer:
(Found a good explanation here: https://icevanila.com/question/cannot-update-state-in-react-after-using-d3csv-to-load-data)
class Card extends React.Component {
state = {
data: []
};
componentDidMount() {
const self = this;
d3.csv(data).then(function(data) {
self.setState({ data: data });
});
function callback(data) {
this.setState({ data: data });
}
d3.csv(data).then(callback.bind(this));
}
render() {
return (
<div>
{this.state.data.map(item => (
<div className="card" key={item.key}>
<h1>{item.quote}</h1>
</div>
))}
</div>
);
}
}
I'd suggest store the response into a state then render the items with a map, something like:
constructor(){
...
this.state = {
data:[],
}
}
componentDidMount() {
...
.then(data => {
this.setState({
data,
})
})
}
render() {
return (
<div>
{this.state.data.map(item) => (
<div className='card' key={item.key}>
<h1>{item.quote}</h1>
</div>
)}
</div>
)
}
I am new to react, I am trying to pass theme string and and toggleTheme function from parent to child using Context API in react.I am practicing example from React Doc with little modification https://reactjs.org/docs/context.html
my code is as following:
import React from 'react';
const ThemeContext = React.createContext({
theme: 'light',
toggleTheme: () => {}
})
class MouseTracker2 extends React.Component {
constructor(props) {
super(props);
this.toggleTheme = () => {
this.setState(state => ({
theme:
state.theme === 'dark'
? 'light'
: 'dark',
}));
};
this.state={
theme: 'dark',
toggleTheme: this.toggleTheme
}
}
render() {
return (
<div>
<ThemeContext.Provider value={this.state}>
<Abc />
</ThemeContext.Provider>
</div>
)
}
}
class Abc extends React.Component {
render() {
return (
<div>
<ThemeContext.Consumer>
{({theme,toggleTheme}) => {return(<Def theme={theme} onClick=
{toggleTheme} />)}}
</ThemeContext.Consumer>
</div>
)
}
}
class Def extends React.Component {
render() {
return (
<div>
<p>efgh</p>
<div>{this.props.theme}</div>
</div>
)
}
}
export default MouseTracker2
In above Code, Context is passing string from parent to child properly. However, it is not passing function from parent to child.
Thanks in Advance :)
The toggleTheme function is passed on to Def by the name onClick and hence this.props.toggleTheme is unavailable and can be accessed by this.props.onClick
class MouseTracker2 extends React.Component {
constructor(props) {
super(props);
this.toggleTheme = () => {
this.setState(state => ({
theme: state.theme === "dark" ? "light" : "dark"
}));
};
this.state = {
theme: "dark",
toggleTheme: this.toggleTheme
};
}
render() {
return (
<div>
<ThemeContext.Provider value={this.state}>
<Abc />
</ThemeContext.Provider>
</div>
);
}
}
class Abc extends React.Component {
render() {
return (
<div>
<ThemeContext.Consumer>
{({ theme, toggleTheme }) => {
return <Def theme={theme} onClick={toggleTheme} />;
}}
</ThemeContext.Consumer>
</div>
);
}
}
class Def extends React.Component {
render() {
return (
<div>
<p>efgh</p>
<div>{this.props.theme}</div>
<button onClick={this.props.onClick}>Toggle</button>
</div>
);
}
}
Working Codesandbox
I am trying to build a functionality in react.
The problem goes like this: I have three components ComponentA, ComponentB, ComponentC. ComponentC can be rendered as a child of either ComponentA or ComponentB.
What I want to achieve is that at any point of time there is only one instance of ComponentC present in the DOM, can be either as a child of ComponentA or ComponentB, both of which are always present.
class ComponentA extends Component {
render() {
return (
<ComponentC />
);
}
}
class ComponentB extends Component {
render() {
return (
<ComponentC />
);
}
}
class ComponentC extends Component {
render() {
return (
<div>This is ComponentC </div>
);
}
}
I tried to build a way to unmount any instance of ComponentC (if present) whenever it is mounted but couldn't succeed.
Implementing a reference counter with hooks does the job:
function Highlander(Component) {
let immortals = 0
return function(props) {
const [onlyOne, thereIsOnlyOne] = React.useState(false)
React.useEffect(() => {
immortals++
if (immortals === 1) {
thereIsOnlyOne(true)
}
() => {
immortals--
}
}, [])
return onlyOne ? <Component { ...props}/> : <React.Fragment></React.Fragment>
}
}
function Test() {
return "test"
}
const OnlyOneTest = Highlander(Test)
ReactDOM.render(<React.Fragment>
<OnlyOneTest />
<OnlyOneTest />
<OnlyOneTest />
</React.Fragment>, document.getElementById("react"));
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
Starring Highlander as a HoC, which "singletons" the composed component (get the Highlander reference ? uhuh).
There are a few ways you can do this. The simplest method that comes to mind is simply to check the DOM if said node exists. If it exists, just render nothing or prevent the state from toggling it.
class MyApp extends React.Component {
constructor() {
super();
this.state = {
showA: false,
showB: false
};
}
toggleA = () => {
if(document.getElementById("component_c") && !this.state.showA) return;
this.setState((prevState) => ({showA: !prevState.showA}));
}
toggleB = () => {
if(document.getElementById("component_c") && !this.state.showB) return;
this.setState((prevState) => ({showB: !prevState.showB}));
}
render() {
return(
<div>
<div>
<button onClick={this.toggleA}>Show/Hide A</button>
<button onClick={this.toggleB}>Show/Hide B</button>
</div>
{this.state.showA && <ComponentA />}
{this.state.showB && <ComponentB />}
</div>
);
}
}
class ComponentA extends React.Component {
render() {
return (
<ComponentC parent="A" />
);
}
}
class ComponentB extends React.Component {
render() {
return (
<ComponentC parent="B" />
);
}
}
class ComponentC extends React.Component {
render() {
return (
<div id="component_c">{"This is ComponentC, rendered from component" + this.props.parent}</div>
);
}
}
ReactDOM.render(<MyApp />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>