Gatsby - Updating context data on page load - reactjs

I am using the Gatsby layout plugin, and want to update the state in the context provider with the page that is currently displayed. I can get it to update on a button click, as shown in this code:
import ContextConsumer from "../components/Context.js"
class Portfolio extends React.Component {
constructor (props){
super(props)
this.state = {
page: "portfolio"
}
}
render(){
return (
<React.Fragment>
<ContextConsumer>
{({ data, set }) => (
<button onClick={() => set({curPage:this.state.page})}>Click</button>
)}
</ContextConsumer>
<h1>This is the portfolio page</h1>
</React.Fragment>
)
}
}
However, I want the context to be updated on page load. I have tried using an immediately invoked arrow function, but this causes an error, "Maximum update depth exceeded".
Update 10.2: This is still confusing me. I can update and read context successfully from a button click, but still can't get it to work on page load, or from a lifecycle method. If you look at this sandbox, you will see what i mean: codesandbox.io/s/thirsty-frog-ocnzl. Clicking to the about page works fine; if you then click on "changer", it updates and successfully reads the new state from the context. However, if you click to the portfolio page, where i try to update on first render, it throws a bunch of errors. Thanks again for any help.

For the benefit of anyone else struggling with this I have found one solution, which is to wrap your component in a higher order component and pass in the context data as props. Here is an example:
//This is the original component
class Portfolio extends React.Component {
constructor(props) {
super(props)
}
componentDidMount() {
this.props.set({ curPage: "portfolio" })
}
render(){
return(
<h1>This is the {this.props.data.curPage} page</h1>
);
}
}
//...and this is the component that wraps it to pass in context data as a prop.
const PortfolioWrap = () => (
<ContextConsumer>
{({ data, set }) => (
<Portfolio data={data} set={set}/>
)}
</ContextConsumer>
)

Related

react recreating a component when I don't want to

I'm super new to react, this is probably a terrible question but I'm unable to google the answer correctly.
I have a component (CogSelector) that renders the following
import React from "react"
import PropTypes from "prop-types"
import Collapsible from 'react-collapsible'
import Cog from './cog.js'
const autoBind = require("auto-bind")
import isResultOk from "./is-result-ok.js"
class CogSelector extends React.Component {
constructor(props) {
super(props)
this.state = {
docs: null,
loaded: false,
error: null
}
autoBind(this)
}
static get propTypes() {
return {
selectCog: PropTypes.func
}
}
shouldComponentUpdate(nextProps, nextState){
if (nextState.loaded === this.state.loaded){
return false;
} else {
return true;
}
}
componentDidMount() {
fetch("/api/docs")
.then(isResultOk)
.then(res => res.json())
.then(res => {
this.setState({docs: res.docs, loaded: true})
}, error => {
this.setState({loaded: true, error: JSON.parse(error.message)})
})
}
render() {
const { docs, loaded, error } = this.state
const { selectCog } = this.props
if(!loaded) {
return (
<div>Loading. Please wait...</div>
)
}
if(error) {
console.log(error)
return (
<div>Something broke</div>
)
}
return (
<>
Cogs:
<ul>
{docs.map((cog,index) => {
return (
<li key={index}>
<Cog name={cog.name} documentation={cog.documentation} commands={cog.commands} selectDoc={selectCog} onTriggerOpening={() => selectCog(cog)}></Cog>
</li>
// <li><Collapsible onTriggerOpening={() => selectCog(cog)} onTriggerClosing={() => selectCog(null)} trigger={cog.name}>
// {cog.documentation}
// </Collapsible>
// </li>
)
})}
{/* {docs.map((cog, index) => { */}
{/* return ( */}
{/* <li key={index}><a onClick={() => selectCog(cog)}>{cog.name}</a></li>
)
// })} */}
</ul>
</>
)
}
}
export default CogSelector
the collapsible begins to open on clicking, then it calls the selectCog function which tells it's parent that a cog has been selected, which causes the parent to rerender which causes the following code to run
class DocumentDisplayer extends React.Component{
constructor(props) {
super(props)
this.state = {
cog: null
}
autoBind(this)
}
selectCog(cog) {
this.setState({cog})
}
render(){
const { cog } = this.state
const cogSelector = (
<CogSelector selectCog={this.selectCog}/>
)
if(!cog) {
return cogSelector
}
return (
<>
<div>
{cogSelector}
</div>
<div>
{cog.name} Documentation
</div>
<div
dangerouslySetInnerHTML={{__html: cog.documentation}}>
</div>
</>
)
}
}
export default DocumentDisplayer
hence the cogSelector is rerendered, and it is no longer collapsed. I can then click it again, and it properly opens because selectCog doesn't cause a rerender.
I'm pretty sure this is just some horrible design flaw, but I would like my parent component to rerender without having to rerender the cogSelector. especially because they don't take any state from the parent. Can someone point me to a tutorial or documentation that explains this type of thing?
Assuming that Collapsible is a stateful component that is open by default I guess that the problem is that you use your component as a variable instead of converting it into an actual component ({cogSelector} instead of <CogSelector />).
The problem with this approach is that it inevitably leads to Collapsible 's inner state loss because React has absolutely no way to know that cogSelector from the previous render is the same as cogSelector of the current render (actually React is unaware of cogSelector variable existence, and if this variable is re-declared on each render, React sees its output as a bunch of brand new components on each render).
Solution: convert cogSelector to a proper separated component & use it as <CogSelector />.
I've recently published an article that goes into details of this topic.
UPD:
After you expanded code snippets I noticed that another problem is coming from the fact that you use cogSelector 2 times in your code which yields 2 independent CogSelector components. Each of these 2 is reset when parent state is updated.
I believe, the best thing you can do (and what you implicitly try to do) is to lift the state up and let the parent component have full control over all aspects of the state.
I solved this using contexts. Not sure if this is good practice but it certainly worked
render() {
return (
<DocContext.Provider value={this.state}>{
<>
<div>
<CogSelector />
</div>
{/*here is where we consume the doc which is set by other consumers using updateDoc */}
<DocContext.Consumer>{({ doc }) => (
<>
<div>
Documentation for {doc.name}
</div>
<pre>
{doc.documentation}
</pre>
</>
)}
</DocContext.Consumer>
</>
}
</DocContext.Provider>
)
}
then inside the CogSelector you have something like this
render() {
const { name, commands } = this.props
const cog = this.props
return (
//We want to update the context object by using the updateDoc function of the context any time the documentation changes
<DocContext.Consumer>
{({ updateDoc }) => (
<Collapsible
trigger={name}
onTriggerOpening={() => updateDoc(cog)}
onTriggerClosing={() => updateDoc(defaultDoc)}>
Commands:
<ul>
{commands.map((command, index) => {
return (
<li key={index}>
<Command {...command} />
</li>
)
}
)}
</ul>
</Collapsible>
)}
</DocContext.Consumer>
)
}
in this case it causes doc to be set to what cog was which is a thing that has a name and documentation, which gets displayed. All of this without ever causing the CogSelector to be rerendered.
As per the reconciliation algorithm described here https://reactjs.org/docs/reconciliation.html.
In your parent you have first rendered <CogSelector .../> but later when the state is changed it wants to render <div> <CogSelector .../></div>... which is a completely new tree so react will create a new CogSelector the second time

Persist dom state on route change

I have two components each on separate routes. I would like to know how I can keep the DOM elements in the same state on route change. For example I would like for all the DOM elements to have the same css classes applied as before the route change when navigating back to the same component.
I have tried redux persist and using nested routes with switch but none of these seem to work. From the research I have done it appears that React always mounts and unmount the component on route change and I haven't' been able to find a way to prevent this happening.
I would like for the red background color to remain when going back to test1.
class test1 extends React.Component {
constructor(props) {
super(props);
}
addClassFucn = event => {
$(event.target).parent().css("background-color", "red")
}
renderButton() {
return (
<div>
<div>
<button onClick={this.addClassFucn}>Click me</button>
<Link to="/test2" className="ui button primary back" >
test2
</Link>
</div>
</div>
)
}
render() {
return (
<div>
<div>This is test 1{this.renderButton()}</div>
</div>
);
}
}
export default test1;
class test2 extends React.Component {
constructor(props) {
super(props);
}
renderButton() {
return (
<div>
<Link to="/test1" className="ui button primary back" >
back
</Link>
</div>
)
}
render() {
return (
<div>
<div>This is test 2{this.renderButton()}</div>
</div>
);
}
}
export default test2;
It really depends on the logic of how you maintain the state of your component.
Redux persist should work. Just persist all the state that affects how the DOM currently displayed. Afterwards, inside the component, you should do a check whether there is a persisted state or not. If it is then you shouldn't do any change and just render.
You can use react's shouldComponentUpdate() which by default returns true allowing the component to re render. If useful than you can add the logic to return false which won't all the component to re-render. This is not recognized as best practice though you can refer this link for more details.

react scroll to component

I have 3 components which is my site. Each component js-file is loaded and all 3 shows on one page like this:
Topmenu
SectionOne
SectionTwo
In the Topmenu component I have a menu only. I’ve tried to setup the scrollToComponent onClick at a menu field (SectionOne). But I cannot figure out how to get it to scroll to SectionOne when clicked?
I know this.sectionOne is a ref to an internal ref, right? But how to direct it to a ref inside the “sectionOne.js” file?
I have the following inside my TopMenu.js file
onClick={() => scrollToComponent(this.sectionOne , { offset: 0, align: 'top', duration: 1500})}
To forward the ref to somewhere inside the component, you can use the logic of forwardRef.
This means that we create the ref in the parent component and pass the same to the component which passes it down to DOM element inside it.
class App extends React.Component {
constructor(props) {
super(props);
this.section1 = React.createRef();
this.section2 = React.createRef();
this.scrollToContent = this.scrollToContent.bind(this);
}
scrollToContent(content) {
switch(content) {
case 1:
this.section1.current.scrollIntoView({behavior: 'smooth'});
break;
case 2:
this.section2.current.scrollIntoView({behavior: 'smooth'});
}
}
render() {
return (
<main>
<Menu goTo={this.scrollToContent} />
<div className='main'>
<Section1 ref={this.section1} />
<Section2 ref={this.section2} />
</div>
</main>
);
}
}
and then, inside Section1,
const Section1 = React.forwardRef((props, ref)=>{
return (
<section ref={ref} className='section'>
<p>Section 1</p>
</section>
);
});
You can see a working sample here
I'm not using scroll-to-component package, but the same thing applies.
Forward Refs are supported only in React 16.3 now, you can use this alternative approach if you need the feature in lower versions.

setState on unmounted component error due as I need to use 2 hooks

I using the std:accounts-ui library for Meteor with React. I need to redirect users to a new page when they register for the first time or login.
I have separate login and register pages, but as Im also using the accounts-facebook package users can login from the register page if they use the facebook button.
For this reason I need to use both onPostSignUpHook and onSignedInHook hooks. My code is working but when users sign up via email I get this error:
Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.
Please check the code for the LoginForm component.
class Register extends React.Component {
constructor(props) {
super(props);
this.state = { redirect: false };
}
handleRedirect() {
if (this.state.redirect === false) {
this.setState({ redirect: true });
}
}
render() {
if (this.state.redirect) return <Redirect to="/people/me" />;
return (
<div className="RegLog RegLog--register">
<h1>Register</h1>
<Accounts.ui.LoginForm
formState={STATES.SIGN_UP}
onPostSignUpHook={() => this.handleRedirect()}
onSignedInHook={() => this.handleRedirect()}
/>
<p>
Already have an account?{' '}
<Link className="link" to="/login">
Login
</Link>
</p>
</div>
);
}
}
export default Register;

Reactjs - Show loading image immediately after click on Link until child isn't fully rendered

I am rendering complex canvas with fabricjs. It takes few second, around 3+-. Is there way to render this page immediately without waiting for render, and show some loading image until canvas can be show instantly?
My parent's component -
class GraphPage extends React.Component {
constructor(props) {
super(props);
this.state = {
loaded: false,
print: null
}
}
render(){
console.log('render');
const graphList = graphStore.getGraphValue();
var {data} = this.props;
return(
<div>
<div className="inputs">
<Inputs modal={false} unit='%' list={graphList.rotation} descTitle={data.graph.machine}/>
<Inputs modal={false} unit='mm' list={graphList.machine} descTitle={data.graph.rotation}/>
</div>
<div className="graph">
<Print />
</div>
</div>
)
}
}
Print is my canvas component.
If print is the fabric JS component I would pass an onLoaded callback to it and then when the fabric.Image object is done loading you can call that function and set the loaded state for this component. Then this function can render something different based on that state.

Resources