I am new to react and this is more of a design question where I intend to
1) Download Components from static hosted site and display them.
2) Get all data from Components through (lets say) a download option
Based on my practice, I learnt that parent component passes a callback/onChangehandle to child as props and it maintains a copy of the state of child components though child components render the HTML. However, I want to dynamically download components from API/Static hosting based on user's input and then get a string representation/state of data from all these components. Each component would have different state data and thus I cannot write a common onChangehandle.
Question:-
1) Is such a design possible with react ? Will each component be able to upload its lib to cloud ?
2) Can we enforce methods to be written on each component so that parent can call each component's specific method and get the data ?
Pseudo Code I am looking for:-
class Parent extends Component {
downloadData = (event) => {
for(childComponents 1 to n){
all_data += childComponent.getState()
}
pdf(all_data)
}
render(){
// Render Child components
for(childComponents 1 to n) {
ChildComponent = // Load from API/Static hosting
}
<button onClick="downloadData"/>
}
}
What you want to accomplish is definitely possible. I am not really into class components so I wrote the example with react hooks.
What you can do to load data from an external component is the following: Pass down a callback into the component which you call on a successful load to receive the data. Nevertheless I would not suggest to do so. I would fetch the data from a different api than the components. You should always split data to visualise and components which represent the user interface.
Code to load an external react module:
import React, { useEffect, useState, useRef } from 'react'
export function Loader({ id }) {
const [loading, setLoading] = useState(true)
const ref = useRef()
const [data, setData] = useState({})
useEffect(() => {
async function fetchReactComponent() {
await import(
/*webpackIgnore: true*/ `https://example.com/react-components/${id}`
).then(module => {
setLoading(false)
const node = module["ReactComponent"](data, setData)
if (ref.current) {
ref.current.innerHTML = ''
if (typeof node === 'string') {
ref.current.innerHTML = node
} else {
ref.current.appendChild(node)
}
}
})
}
fetchReactComponent()
// eslint-disable-next-line
}, [id])
return loading ? <div>Loading...</div> : <div ref={ref} />
}
External React module:
function ReactComponent(data, setData) {
// do something with the data
return '<h1>A component</h1>'
}
export { ReactComponent }
Related
I am in the process to convert some old class based react components to functional components.
Lets say I have following Dashboard component, consisting of Input and Chart:
import React from 'react';
import Calculation from './calculation';
export default function Dashboard(properties){
const [data, setData] = React.useState();
const input = renderInput(setData);
const chart = renderChart(data);
React.useEffect(async ()=> {
//initialize chart data if input is valid
if(input.isValid){
const inputData = input.data;
const result = await Calculation.calculate(inputData);
setData(data);
}
})
return <>
{input}
{chart}
</>
}
...
function renderInput(setData){
const dataChanged = async inputData => {
const result = await Calculation.calculate(inputData);
setData(result);
}
...
}
function renderChart(data){
...
}
If Input is a class based component, I can define accessible attributes isValid, data for it, e.g.
get isValid(){
return this._validata(this.data);
}
Those attributes can be accessed during the initialization step, that is currently defined in the scope of the Dashboard.
Is this still possible, if Input is converted to a functional react component?
a) If so, how can I define and use the attributes "isValid", "data" for the functional component?
b) If not, how should I restructure the above code?
Should I move the initialization and calculation from the Dashboard down into the Input component? Then the Input would know about the Calculation and I would like to avoid that.
I'm dealing with a mix of function components and class components. Every time a click happens in the NavBar I want to trigger the function to validate the Form, it has 5 forms, so each time I'm going to have to set a new function inside the context API.
Context.js
import React, { createContext, useContext, useState } from "react";
const NavigationContext = createContext({});
const NavigationProvider = ({ children }) => {
const [valid, setValid] = useState(false);
const [checkForm, setCheckForm] = useState(null);
return (
<NavigationContext.Provider value={{ valid, setValid, checkForm, setCheckForm }}>
{children}
</NavigationContext.Provider>
);
};
const useNavigation = () => {
const context = useContext(NavigationContext);
if (!context) {
throw new Error("useNavigation must be used within a NavigationProvider");
}
return context;
};
export { NavigationProvider, useNavigation, NavigationContext};
Form.js
import React, { Component } from "react";
import { NavigationContext } from "../hooks/context";
class Something extends Component {
static contextType = NavigationContext;
onClickNext = () => {
// This is the funcion I want to set inside the Context API
if(true){
return true
}
return false;
};
render() {
const { setCheckForm } = this.context;
setCheckForm(() => () => console.log("Work FFS"));
return (
<>
<Button
onClick={this.onClickNext}
/>
</>
);
}
}
export default Something;
The problem when setting the function it throws this error:
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
And setting like setCheckForm(() => console.log("Work FFS"));, it triggers when rendered.
Render method of React.Component runs whenever state changes and setCheckForm updates the state whenever that render happens. This creates an infinite loop, this is the issue you are having there.
So, this is a lifecycle effect, you have to use that function inside componentDidMount if you want to set it when the component first loads.
While this solves your problem, I wouldn't suggest doing something like this. React's mental model is top to bottom, data flows from parent to child. So, in this case, you should know which component you are rendering from the parent component, and if you know which component to render, that means you already know that function which component is going to provide to you. So, while it is possible in your way, I don't think it is a correct and Reactish way to handle it; and it is probably prone to break.
I am currently building a Dashboard using React and D3. I'd like to compose each Dashboard as their own component (rather than have a single mega component manage everything) but re-use the child components within each Dashboard which will allow smooth animations and transitioning.
The following is a very rough example of what I'm trying to achieve.
const Dashboard1 = () => {
const data = useData('/main');
return <ChartComponent data={data}/>;
};
const SubDashboard = () => {
const data = useData('/sub');
return <ChartComponent data={data}/>;
};
const App = (props) => {
return props.topLevel ? <TopDashboard/> : <SubDashboard/>;
};
The issue I am finding with this approach is that <ChartComponent> will un-mount and re-mount when navigating between each Dashboard (due to the parents being different) which causing the page to flicker and all visuals to be redrawn which is expensive for some of the more complex visuals. What I would like to happen is for the same instance of <ChartComponent> to be used on both dashboards between each render so that it doesn't unmount, gets the new data passed into it and can animate from the previous values to the new ones.
Are there are ways or patterns to achieve this?
You need a parent item that stores all the data and passes it down to child components but I'd suggest to do this with redux.
const Dashboard1 = (props) => {
return <ChartComponent data={props.data.main}/>;
};
const SubDashboard = (props) => {
return <ChartComponent data={props.data.sub}/>;
};
const App = (props) => (
// this should be load elsewhere, store in redux and use it here once loaded
const data = {
main: useData('/main')
sub: useData('/sub'),
};
return props.topLevel ? <TopDashboard data={data} /> : <SubDashboard data={data} />;
};
you need to use this react reparating npm module to do that. React doesn't provide support for it in-built.
https://paol-imi.github.io/react-reparenting/
So here's the situation - When a Link is clicked the nprogress bar will start and I want react-router to only replace the current component with the matched route once that's done loading asynchronously.. just like in instagram..
But I am only getting this -
Here's my HOC to load component asynchronously --
import React, { useEffect, useState } from "react";
import nprogress from "nprogress";
import "nprogress/nprogress.css";
export default importComponent => props => {
const [loadedComponent, setComponent] = useState(null);
// this works like componentwillMount
if (!nprogress.isStarted()) nprogress.start();
if (loadedComponent) nprogress.done();
useEffect(() => {
let mounted = true;
mounted &&
importComponent().then(
({ default: C }) => mounted && setComponent(<C {...props} />)
);
// componentUnMount
return () => (mounted = false);
}, []);
// return the loaded component
const Component = loadedComponent || <div style={{ flexGrow: 1 }}>..</div>;
return Component;
};
I didn't find a solution to this anywhere on the internet.. so I am asking this question here in stackoverflow. I am hoping someone here can solve this.
Edit: 15-Nov-2021
The old approach doesn't work with routes that uses route params or query such as useLocation, useRouteMatch, etc.
The one I am using now is to use React.lazy, React.Suspense and updating the fallback prop whenever a page is rendered. So when the next page is being loaded, the fallback will be shown which is basically the same component instance as current page. Moreover, with this new approach, you can render the next page whenever you like; after fetching data from backend, or after animation, etc.
Here is a demo and source code.
I am trying to implement a shared state into my application using the React context api.
I am creating an errorContext state at the root of my tree. The error context looks like so:
// ErrorContext.js
import React from 'react';
const ErrorContext = React.createContext({
isError: false,
setError: (error) => {}
});
export default ErrorContext;
Desired Result
I would like to update (consume) this context from anywhere in the app (specifically from within a promise)
Ideally the consume step should be extracted into a exported helper function
Example Usage of helper function
http.get('/blah')
.catch((error) => {
HelperLibrary.setError(true);
})
Following the react context docs:
I can create a provider like so :
class ProviderClass {
state = {
isError: false,
setError: (error) => {
this.state.isError = error;
}
}
render() {
return (
<ErrorContext.Provider value={this.state}>
{this.props.children}
</ErrorContext.Provider>
)
}
}
Then I can consume this provider by using the Consumer wrapper from inside a render call:
<ErrorContext.Consumer>
{(context) => {
context.setError(true);
}}
</ErrorContext.Consumer>
The Problem with this approach
This approach would require every developer on my team to write lots of boilerplate code every-time they wish to handle a web service error.
e.g. They would have to place ErrorContext.Consumer inside the components render() method and render it conditionally depending on the web service response.
What I have tried
Using ReactDOM.render from within a helper function.
const setError = (error) =>{
ReactDOM.render(
<ErrorContext.Consumer>
// boilerplate that i mentioned above
</ErrorContext.Consumer>,
document.getElementById('contextNodeInDOM')
) }
export default setError;
Why doesn't this work?
For some reason ReactDOM.render() always places this code outside the React component tree.
<App>
...
<ProviderClass>
...
<div id="contextNodeInDOM'></div> <-- even though my node is here
...
</ProviderClass>
</App>
<ErrorContext.Consumer></ErrorContext.Consumer> <-- ReactDOM.render puts the content here
Therefore there is no context parent found for the consumer, so it defaults to the default context (which has no state)
From the docs
If there is no Provider for this context above, the value argument
will be equal to the defaultValue that was passed to createContext().
If anyone can assist me on my next step, I am coming from Angular so apologies if my terminology is incorrect or if I am doing something extremely stupid.
You can export a HOC to wrap the error component before export, eliminating the boilerplate and ensuring that the context is provided only where needed, and without messing with the DOM:
// error_context.js(x)
export const withErrorContext = (Component) => {
return (props) => (
<ErrorContext.Consumer>
{context => <Component {...props} errorContext={context} />}
</ErrorContext.Consumer>
)
};
// some_component.js(x)
const SomeComponent = ({ errorContext, ...props }) => {
http.get('/blah')
.catch((error) => {
errorContext.setError(true);
})
return(
<div></div>
)
};
export default withErrorContext(SomeComponent);
Now that React 16.8 has landed you can also do this more cleanly with hooks:
const SomeComponent = props => {
const { setError } = useContext(ErrorContext)
http.get("/blah").catch(() => setError(true))
return <div />
}
Following the react context docs:
I can create a provider like so :
class ProviderClass {
state = {
isError: false,
setError: (error) => {
this.state.isError = error;
}
}
I don't think so - there should be setState used. There is a general rule in react "don't mutate state - use setState()" - abusing causes large part of react issues.
I have a feeling you don't understand context role/usage. This is more like a shortcut to global store eliminating the need of explicitly passing down props to childs through deep components structure (sometimes more than 10 levels).
App > CtxProvider > Router > Other > .. > CtxConsumer > ComponentConsumingCtxStorePropsNMethods
Accessing rendered DOM nodes with id is used in some special cases, generally should be avoided because following renders will destroy any changes made externally.
Use portals if you need to render sth somewhere outside of main react app html node.