React -- Trigger chart download method from sibling component - javascript

Looking for some guidance on how to structure the below components to trigger an (export) method in one component from another 'sibling' component.
Using a download button contained in <DownloadChartButton> in the Card Header, I want to download a chart contained in my PaginatedRechartsChart component as a PNG. I currently have the components structured like below. DownloadChartButton needs to access the recharts chart contained in my <PaginatedRechartsChart> component. Hoping to keep DashboardCard, DownloadChartButton, and PaginatedRechartsChart generic so they can be applied in many places, hence the current structure.
Question: Is there a better way to structure my components to avoid the React anti-pattern of triggering child methods from parent components?
Thank you for your help!
Pseudo-code:
export const LineChartByDay = () => {
const [data, setData] = React.useState([{'date':'2021-01-01': 'count':34})
return (
<DashboardCard
headerItems={<DownloadChartButton data={data}/>}
>
<PaginatedRechartsChart data={data}/>
</DashboardCard>
)
}
import {LineChart} from 'recharts'
export const PaginatedRechartsChart = (data) => {
// ... extra code for client-side pagination, etc.
return (
<ResponsiveContainer>
<LineChart data={data}/>
</ResponsiveContainer>
)
}
export const DashboardCard = ({ title, actions, children, error, loading }: { title: string, actions?: React.ReactNode, children?: React.ReactNode, error?: any, loading?: boolean }) =\> {
return (
<Card>
<CardHeader
title={title}
action={actions}
></CardHeader>
<CardContent>
{error && <ErrorAlertBar title={error}/>}
{loading && <CircularProgress/>}
{children}
</CardContent>
</Card>
)
}
export const DownloadChartButton = (data) => {
// ... CSV download logic (takes a list of objects in data, exports to CSV)
// ... PNG download logic
const [getPng, { ref }] = useCurrentPng();
//Ideally we'd have chart download logic in this component so we don't have to copy it into every chart component
const handleDownload = React.useCallback(async () => {
const png = await getPng();
// Verify that png is not undefined
if (png) {
// Download with FileSaver
FileSaver.saveAs(png, 'myChart.png');
}
}, [getPng]);
return (
<MenuButton>
<MenuItem>CSV</MenuItem>
<MenuItem>PNG</MenuItem>
</MenuButton>
)
}
From what I see online, my current thought is using a ref in the parent component and passing that to PaginatedRechartsChart and DownloadChartButton... but I'm seeing online that this is an anti-pattern for React (i.e. https://stackoverflow.com/a/37950970/20391795)

Related

Next.js static generation with dynamic imports

I have a Template component which builds a page based on a json structure. The json contains multiple objects including keys, which then define what component is to be used.
Like this:
const componentList: Record<string, any> = {
BlockCards: dynamic<Data.BlockCards>(() =>
import("../molecule/BlockCards").then(module => module.BlockCards)
),
BlockNews: dynamic<Data.BlockNews>(() =>
import("../molecule/BlockNews").then(module => module.BlockNews)
),
PageHeader: dynamic<Data.PageHeader>(() =>
import("../atom/PageHeader").then(module => module.PageHeader)
),
};
interface TemplateProps {
data: Data.Main;
}
export const Template: React.FC<TemplateProps> = ({ data }) => {
return (
<React.Fragment>
{data.content.map((item, index) => {
const Component = componentList[item.component.type];
if (!Component) return null;
return (
<Block key={index}>
<Component {...item.component} {...props} />
</Block>
);
})}
</React.Fragment>
);
};
This works, but the components are lazy loaded on the client's side. I want the rendering to happen on build, just like with getStaticProps. Importing my components without using dynamic works and the components will be prerendered thanks to next's static optimization. But since this one Template components imports multiple components that might not be used by some pages, I need the components to be only imported if used - how can I do so?

How to reduce render time while rendering huge list of items in Ionic React

I am using IonicReact to build a mobile app. I just wanna render a list of items from a local JSON file (JSON file contains an array of more than 1000 items). I map through the array items and print them by wrapping them in <IonItem>.It takes few seconds to iterate through all the items and displaying them in the browser. Please help me, how do I optimize render time, load the data fast from JSON.
Code is below:
//necessary imports
import data from './data.json';
const ListItems: React.FC = () => {
const showItems = data
.map((topic) => {
return (<IonItem>{topic}</IonItem>);})
return (
<IonPage>
<IonContent fullscreen className="bg-style">
<IonList>{showItems}</IonList>
</IonContent>
</IonPage>
);
};
export default ListItems;
Basically it should be two methods: 1. virtual scroll. 2. infinite scroll by cutting your large array to small chunks.
However, in your case, the virtual scroll method should be much better.
Ionic virtual scroll doesn't support react. But I found an article and reimplemented the VirtualScrollChild component:
https://medium.com/#alvinnguyen116/virtual-and-infinite-scrolling-in-react-d56a05976cd2
import React from "react"
import { useInView } from "react-intersection-observer"
interface Props {
height?: number
}
export const VirtualScrollChild: React.FC<Props> = (props) => {
const [ref, inView] = useInView()
const style = {
height: `${props.height ? props.height : 40}px`,
overflow: "hidden",
}
return (
<div style={style} ref={ref}>
{inView ? props.children : null}
</div>
)
}
usage:
<IonList>
{your_data_list.map((data) => (
<VirtualScrollChild height={your_height}>
{data...}
</VirtualScrollChild>
))}
</IonList>

Handle React re-rendering

I have a similar situation like the one in the sandbox.
https://codesandbox.io/s/react-typescript-fs0em
Basically what I want to achieve is that Table.tsx is my base component and App component is acting like a wrapper component. I am returning the JSX from the wrapper file.
Everything is fine but the problem is whenever I hover over any name, getData() is called and that is too much rerendering. Here it is a simple example but in my case, in real, the records are more.
Basically Table is a generic component which can be used by any other component and the data to be displayed in can vary. For e.g. rn App is returning name and image. Some other component can use the Table.tsx component to display name, email, and address. Think of App component as a wrapper.
How can I avoid this getData() to not to be called again and again on hover?
Can I use useMemo or what approach should I use to avoid this?
Please help
Every time you update the "hover" index state in Table.jsx it rerenders, i.e. the entire table it mapped again. This also is regenerating the table row JSX each time, thus why you see the log "getData called!" so much.
You've effectively created your own list renderer, and getData is your "renderRow" function. IMO Table shouldn't have any state and the component being rendered via getData should handle its own hover state.
Create some "row component", i.e. the thing you want to render per element in the data array, that handles it's own hover state, so when updating state it only rerenders itself.
const RowComponent = ({ index, name }) => {
const [hov, setHov] = useState();
return (
<div
key={name}
onMouseEnter={() => setHov(index)}
onMouseLeave={() => setHov(undefined)}
style={{ display: "flex", justifyContent: "space-around" }}
>
<div> {name} </div>
<div>
<img
src={hov === index ? img2 : img1}
height="30px"
width="30px"
alt=""
/>
</div>
</div>
);
};
Table.jsx should now only take a data prop and a callback function to render a specific element, getData.
interface Props {
data: string[];
getData: () => JSX.Element;
}
export const Table: React.FC<Props> = ({ data, getData }) => {
return (
<div>
{data.map((name: string, index: number) => getData(name, index))}
</div>
);
};
App
function App() {
const data = ["Pete", "Peter", "John", "Micheal", "Moss", "Abi"];
const getData = (name: string, index: number, hov: number) => {
console.log("getData called!", index);
return <RowComponent name={name} index={index} />;
};
return <Table data={data} getData={getData} />;
}

React HOC working on some but not other components

I'm using a HOC component to bind an action to many different types of element, including SVG cells, which, when an onClick is bound normally, it works, but when I use my HOC it returns un-intended results.
Minimally reproducible example: https://codesandbox.io/s/ecstatic-keldysh-3viw0
The HOC component:
export const withReport = Component => ({ children, ...props }) => {
console.log(Component); //this only prints for ListItem elements for some reason
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
console.log('clicked!'); //even this wont work on some.
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return (
<Component onClick={handleClick} {...props}>
{children}
</Component>
);
};
Usage working:
const ListItemWIthReport = withReport(ListItem); //list item from react-mui
{items.map((item, key) => (
<ListItemWithReport report={item.report} key={key} button>
{/* listitem children*/}
</ListItemWithReport>
))}
Usage not working:
const BarWithReport = withReport(Bar); //Bar from recharts
{bars.map((bar, index) => (
<BarWithReport
report={bar.report}
key={index}
dataKey={bar.name}
fill={bar.fill}
/>
))}
The ListItem works 100% as anticipated, however, the bars will not render inside of the BarChart. Similarly, with a PieChart the Cells will actually render, with the correct sizes according to their values, however, props like "fill" do not appear to pass down.
Am I using the HOC incorrectly? I don't see an option other than HOC for the inside of Charts as many types of elements will be considered invalid HTML?
You might be dealing with components that have important static properties that need to be hoisted into the wrapped component or need to have ref forwarding implemented in order for their parent components to handle them. Getting these pieces in place is important, especially when wrapping components where you don't know their internals. That Bar component, for example, does have some static properties. Your HOC is making those disappear.
Here's how you can hoist these static members:
import hoistNonReactStatic from 'hoist-non-react-statics';
export const withReport = Component => {
const EnhancedComponent = props => {
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return (
<Component onClick={handleClick} {...props}/>
);
};
hoistNonReactStatic(EnhancedComponent, Component);
return EnhancedComponent;
};
Docs on hoisting statics and ref forwarding can be found in this handy guide to HOCs.
There may be some libraries that can take care of all these details for you. One, addhoc, works like this:
import addHOC from 'addhoc';
export const withReport = addHOC(render => {
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return render({ onClick: handleClick });
});
Of course, if the parent component is checking child components by type explicitly, then you won't be able to use HOCs at all. In fact, it looks like recharts has that issue. Here you can see the chart is defined in terms of child components which are then searched for explicitly by type.
I think your HOC is invalid, because not every wrapper-Component (e.g. HTML element) is basically clickable. Maybe this snipped can clarify what I am trying to say:
const withReport = Component => (props) => {
const handleClick = () => console.log('whatever')
// Careful - your component might not support onClick by default
return <Component onClick={handleClick} {...props} />
// vs.
return <div onClick={handleClick} style={{backgroundColor: 'green'}}>
<Component {...props} />
{props.children}
</div>
}
// Your import from wherever you want
class SomeClass extends React.Component {
render() {
return <span onClick={this.props.onClick}>{this.props.children}</span>
// vs.
return <span style={{backgroundColor: 'red'}}>
{
// Careful - your imported component might not support children by default
this.props.children
}
</span>
}
}
const ReportedListItem = withReport(SomeClass)
ReactDOM.render(<ReportedListItem>
<h2>child</h2>
</ReportedListItem>, mountNode)
You can have the uppers or the unders (separated by vs.) but not crossed. The HOC using the second return (controlled wrapper-Component) is sure more save.
I've used 4 methods successfully to wrap Recharts components.
First Method
Wrap the component in a HOC and use Object.Assign with some overloads. This breaks some animation and difficult to use an active Dot on lines. Recharts grabs some props from components before rendering them. So if the prop isn't passed into the HOC, then it won't render properly.
...
function LineWrapper({
dataOverload,
data,
children,
strokeWidth,
strokeWidthOverload,
isAnimationActive,
dot,
dotOverload,
activeDot,
activeDotOverload,
...rest
}: PropsWithChildren<Props>) {
const defaultDotStroke = 12;
return (
<Line
aria-label="chart-line"
isAnimationActive={false}
strokeWidth={strokeWidthOverload ?? 2}
data={dataOverload?.chartData ?? data}
dot={dotOverload ?? { strokeWidth: defaultDotStroke }}
activeDot={activeDotOverload ?? { strokeWidth: defaultDotStroke + 2 }}
{...rest}
>
{children}
</Line>
);
}
export default renderChartWrapper(Line, LineWrapper, {
activeDot: <Dot r={14} />,
});
const renderChartWrapper = <P extends BP, BP = {}>(
component: React.ComponentType<BP>,
wrapperFC: React.FC<P>,
defaultProps?: Partial<P>
): React.FC<P> => {
Object.assign(wrapperFC, component);
if (defaultProps) {
wrapperFC.defaultProps = wrapperFC.defaultProps ?? {};
Object.assign(wrapperFC.defaultProps, defaultProps);
}
return wrapperFC;
};
Second Method
Use default props to assign values. Any props passed into the HOC will be overridden.
import { XAxisProps } from 'recharts';
import { createStyles } from '#material-ui/core';
import { themeExtensions } from '../../../assets/theme';
const useStyles = createStyles({
tickStyle: {
...themeExtensions.font.graphAxis,
},
});
type Props = XAxisProps;
// There is no actual implementation of XAxis. Recharts render function grabs the props only.
function XAxisWrapper(props: Props) {
return null;
}
XAxisWrapper.displayName = 'XAxis';
XAxisWrapper.defaultProps = {
allowDecimals: true,
hide: false,
orientation: 'bottom',
width: 0,
height: 30,
mirror: false,
xAxisId: 0,
type: 'category',
domain: [0, 'auto'],
padding: { left: 0, right: 0 },
allowDataOverflow: false,
scale: 'auto',
reversed: false,
allowDuplicatedCategory: false,
tick: { style: useStyles.tickStyle },
tickCount: 5,
tickLine: false,
dataKey: 'key',
};
export default XAxisWrapper;
Third Method
I didn't like this so I've worked around it, but you can extend the class.
export default class LineWrapper extends Line {
render(){
return (<Line {...this.props} />
}
}
Fourth Method
I don't have a quick example of this, but I always render the shape or children and provide functions to help. For example, for bar cells I use this:
export default function renderBarCellPattern(cellOptions: CellRenderOptions) {
const { data, fill, match, pattern } = cellOptions;
const id = _uniqueId();
const cells = data.map((d) =>
match(d) ? (
<Cell
key={`cell-${id}`}
strokeWidth={4}
stroke={fill}
fill={`url(#bar-mask-pattern-${id})`}
/>
) : (
<Cell key={`cell-${id}`} strokeWidth={2} fill={fill} />
)
);
return !pattern
? cells
: cells.concat(
<CloneElement<MaskProps>
key={`pattern-${id}`}
element={pattern}
id={`bar-mask-pattern-${id}`}
fill={fill}
/>
);
}
// and
<Bar {...requiredProps}>
{renderBarCellPattern(...cell details)}
</Bar>
CloneElement is just a personal wrapper for Reacts cloneElement().

How can I return a new array based on a search criteria?

I am trying to figure out how can I develop a search box.
I have this array passengersData which is coming from a redux store.
All of the examples about search functionality on React I've seen are based on local state with the search input in the same component where the rendered data array lives. But I need to pass the value of the search box to the child component, and then, what can I do to perform the search?
This is the child component:
// imports
const PassengerCardBasedOnRoute = ({
passengersData,
searchParam,
}) => {
return (
<View>
{passengersData &&
passengersData.map(info => (
<PassengersInfo
key={info.id}
searchParam={searchParam}
name={info.name}
callModal={popupsModalsActionHandler}
/>
))}
</View>
);
};
// Props validatiom
export default compose(
connect(
store => ({
passengersData: store.homeScreen.passengersData,
}),
dispatch => ({
popupsModalsActionHandler: data => {
dispatch(popupsModalsAction(data));
},
}),
),
)(PassengerCardBasedOnRoute);
And this is the component that will hold the search box:
// imports
// CHILD COMPONENT
import PassengerCardBasedOnRoute from '../PassengerInfo/PassengerCardBasedOnRoute';
class AllPassengersList extends Component {
state = { searchParam: '' };
render() {
const { searchParam } = this.state;
return (
<View>
{isVisible && (
<View>
<TextInput
onChangeText={text => this.setState({ searchParam: text })}
value={searchParam}
/>
</View>
)}
<PassengerCardBasedOnRoute searchParam={searchParam} />
</View>
);
}
}
// Props validation
export default compose(
connect(store => ({
navigationStore: store.homeScreen.navigation,
passengersData: store.homeScreen.passengersData,
})),
)(AllPassengersList);
So, based on my code, how can I grab the value and what should I do to perform the search across passengersData array?
Also, as you may see, passengersData is coming from a redux store.
There are a couple of options you can do to achieve what you want. The first is to simply augment your map to also include a filter. For example:
{passengersData && passengersData
.filter(info => {
// return true here if info matches the searchParam
})
.map(info => ( ...
The other is to move your search params from local state to redux, and perform the filter inside an action that responds to the search param being changed. For simplicity I would go with the first option for now, but if it turns out you need to access the search param in other parts of your app, or do something more complex (like optimise rendering or something), I would move the search stuff into redux.

Categories

Resources