I have struggled with the pass props issue for few days but haven't figure out how to achieve this.
In my Project, I have a rechart page index.js and the inherit relationship looks like: index.js => RechartsComponent => LineChartComponent But I can't prop the list from index.js to its grandchild LineChartComponent to replace the data source of SimpleLineChart. I have tried different ways but nothing works. Any advice will be appreciated.
index.js
render() {
const { rechartdata } = this.props
const { list } = rechartdata
return (
<Page inner>
<RadioGroup
options={chartList}
defaultValue="lineChart"
onChange={this.handleRadioGroupChange}
/>
<div className={styles.chart}>
<ReChartsComponent type={this.state.type} list={list} />
</div>
</Page>
)
}
RechartsComponent.js
const ReChartsComponent = ({ type,list}) => {
if (type === 'areaChart') return <AreaChartComponent />
if (type === 'barChart') return <BarChartComponent />
return <LineChartComponent list={list}/>
}
LineChartComponent.js
const SimpleLineChart = ({list}) => (
<Container>
<LineChart
// data={data}
data={list}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<XAxis dataKey="name" />
<YAxis />
<CartesianGrid strokeDasharray="3 3" />
<Tooltip />
<Legend />
<Line
type="monotone"
dataKey="id"
stroke="#8884d8"
activeDot={{
r: 8,
}}
/>
</LineChart>
</Container>
)
const LineChartPage = () => (
<div className="content-inner">
<Button
type="primary"
style={{
position: 'absolute',
right: 0,
top: -48,
}}
>
<a
href="http://recharts.org/#/en-US/examples/TinyBarChart"
target="blank"
>
Show More
</a>
</Button>
<Row gutter={32}>
<Col {...colProps}>
<Card title="Simple Chart">
<SimpleLineChart />
</Card>
</Col>
<Col {...colProps}>
<Card title="DashedLineChart">
<DashedLineChart />
</Card>
</Col>
<Col {...colProps}>
<Card title="CustomizedDotLineChart">
<CustomizedDotLineChart />
</Card>
</Col>
<Col {...colProps}>
<Card title="VerticalLineChart">
<VerticalLineChart />
</Card>
</Col>
</Row>
</div>
)
export default LineChartPage
Reason is simply what you're exporting for LineChartComponent.js is export default LineChartPage
Hence when you define LineChartPage, you need to have prop for that
const LineChartPage = ({list}) => (
...
)
Something good to have but not mandatory is that having the same file name as component will avoid careless mistake like what you've made, such as below
LineChartComponent.js
const LineChartComponent = ({list})=> (
...
)
export default LineChartComponent;
Keep the name of your component and the file name consistent
First off, in your example, the SimpleLineChart isn't receiving a list from LineChartPage. You would need to add a list prop to LineChartPage and pass that into SimpleLineChart
That said, when I need to pass props past more than one level of depth, I generally switch to the Context API.
I would create a context provider for the state you want to pass down:
import { createContext, FC, useReducer } from "react";
// ...
const InitialState : GrandparentStateI = {
// As you wish...
}
export const GrandparentContext = createContext(InitialState);
// ...
grandparentContextReducer = (state : GrandparentStateI, action : {
type : string,
payload : any
})=>{
switch(action.type){
// ...
}
}
export const GrandparentContextProvider : FC<{}> = ({children})=>{
const [state, dispatch] = useReducer(grandparentContextReducer, InitialState);
return (
<GrandparentContext.Provider value={state}>{children}</GrandparentContext.Provider>
)
}
Define a hook which you will use to access the context:
export const useGrandparentContext = () : GrandparentContextProps =>{
const context = useContext(GrandparentContext);
if(!context._inProvider){ // I usually have an _inProvider member
throw new Error("useGrandparentContext must be called within a GrandparentProvider.");
}
return {
...context,
};
}
Wrap the Grandparent's children in the provider;
const Grandparent : FC<{...}> = ({children})=>{
return (
<div>
// ...
<GrandparentContextProvider>{children}</GrandparentContextProvider>
</div>
)
}
Call the hook from the grandchildren at whatever depth:
const Grandchild : FC<{...}> = ()=>{
const {
...
} = useGrandparentContext();
return (
// ...
)
}
The Context API generally makes refactoring or adding layers of depth much easier than other patterns.
Related
Since I want to add the action after selection in GridHeader, I am following the tutorial to try to put them together:
https://mui.com/x/react-data-grid/selection/#controlled-selection
https://mui.com/x/react-data-grid/column-visibility/#column-visibility-panel
I believe this is a very common and universal requirement. When I select a row or rows, I can't pass the resulting data to the GridToolbar subcomponent. It seems that it does not support passing properties.
There is example code:
export defualt function App(props: any){
const {rows, columns} = props;
const [selectionModel, setSelectionModel] = useState<GridSelectionModel>([]);
return (
<DataGrid components={{ Toolbar: DataToolbar }}
onSelectionModelChange={(newSelectionModel) => {
setSelectionModel(newSelectionModel);
}}
selectionModel={selectionModel}
{...others} />
);
}
function DataToolbar (){
<GridToolbarContainer>
<Auctions />
<GridToolbarQuickFilter />
<GridToolbarColumnsButton />
<GridToolbarDensitySelector />
</GridToolbarContainer>
}
function Auctions (){
const apiRef = useGridApiContext();
const [selected, setSelected] = useState(false);
const handleSelected: GridEventListener<'rowSelectionCheckboxChange'> = (
params, event, details) => {
//Processing Event
setSelected(!selected);
};
useGridApiEventHandler(apiRef, 'rowSelectionCheckboxChange', handleSelected);
return (
<Fragment>
<IconButton disabled={!selected}><DeleteIcon color={selected? "error" : "disabled"} /></IconButton>
</Fragment>
);
}
Many thanks to the MUI team for their help, now it has been solved.
An explanation was found for this problem in here:
https://github.com/mui/mui-x/issues/7377
The Demo in here:
https://codesandbox.io/s/sleepy-lehmann-vgvlq8?file=/demo.tsx
I'm copying it here in the hope that it will be helpful to those who have the same questions
function Auctions({ selectionModel }) {
const hasSelectedRows = selectionModel.length > 0;
return (
<React.Fragment>
<IconButton disabled={!hasSelectedRows}>
<DeleteIcon color={hasSelectedRows ? "error" : "disabled"} />
</IconButton>
</React.Fragment>
);
}
function DataToolbar({ selectionModel }) {
return (
<GridToolbarContainer>
<Auctions selectionModel={selectionModel} />
<GridToolbarQuickFilter />
<GridToolbarColumnsButton />
<GridToolbarDensitySelector />
</GridToolbarContainer>
);
}
export default function FlexLayoutGrid() {
const { data } = useDemoData({
dataSet: "Commodity",
rowLength: 5,
maxColumns: 6
});
const [selectionModel, setSelectionModel] = React.useState<
GridSelectionModel
>([]);
return (
<div style={{ height: 400, width: "100%" }}>
<DataGrid
{...data}
onSelectionModelChange={(newSelectionModel) => {
setSelectionModel(newSelectionModel);
}}
selectionModel={selectionModel}
components={{ Toolbar: DataToolbar }}
checkboxSelection
componentsProps={{
toolbar: {
selectionModel: selectionModel
}
}}
/>
</div>
);
}
I have a swipeable component with an open and close function that I would like to trigger from a child of the component. I don't think using state to do it in this instance would be correct since I want to avoid a re-render while the swipeable component is animating (please correct me if I'm wrong).
I'm using react-native-gesture-handler/swipeable and following their example here
SwipeCard component
import React, { useRef } from 'react';
import { RectButton } from 'react-native-gesture-handler';
import Swipeable from 'react-native-gesture-handler/Swipeable';
import Animated from 'react-native-reanimated';
const AnimatedView = Animated.createAnimatedComponent(View);
export const SwipeCard = ({ children }) => {
let swipeableRow = useRef(null);
let renderRightActions = (_progress, dragX) => {
let scale = dragX.interpolate({
inputRange: [-80, 0],
outputRange: [1, 0],
extrapolate: 'clamp',
});
return (
<RectButton style={styles.rightAction} onPress={close}>
{/* Change it to some icons */}
<AnimatedView style={[styles.actionIcon]} />
</RectButton>
);
};
let close = () => {
swipeableRow?.close();
};
let open = () => {
swipeableRow?.openRight();
};
return (
<Swipeable
ref={swipeableRow}
renderRightActions={renderRightActions}
friction={2}
rightThreshold={40}
>
{children}
</Swipeable>
);
};
Below is the component where I'm using SwipeCard and the Toggle is the event I want to use to fire the open() method in the SwipeCard component.
<Row>
{arr.map((item) => {
return (
<SwipeCard key={item.id}>
<CardContainer>
<CleaningCard
cleaningData={item}
/>
<Toggle onPress={() => {}}>
<Icon name="dots" />
</Toggle>
</CardContainer>
</SwipeCard>
);
})}
</Row>
You can use the render prop pattern and pass close, open as arguments.
Parent component where SwipeCard is used:
<Row>
{arr.map((item) => {
return (
<SwipeCard key={item.id}>
{({ close, open }) => (
<CardContainer>
<CleaningCard cleaningData={item} />
<Toggle
onPress={() => {
// close()/open()
}}
>
<Icon name="dots" />
</Toggle>
</CardContainer>
)}
</SwipeCard>
);
})}
</Row>;
SwipeCard component:
<Swipeable
ref={swipeableRow}
renderRightActions={renderRightActions}
friction={2}
rightThreshold={40}
>
{children({close, open})}
</Swipeable>
We're simply making the children a function that takes an object and returns the JSX. The required object is passed as an argument (children({close, open})).
I have made a call to my api using useEffect and stored the array of items using useState hook but I'm finding it difficult to render those items into a custom component which will also have the data passed.
Here's my react snippets:
export default function CreateCast() {
const [open, setOpen] = useState(false);
const [bibleCastItems, setBibleCastItems] = useState([]);
const classes = useStyles();
const fabStyle = {
bottom: 50.0,
right: 30.0,
position: "fixed"
};
useEffect(()=>{
async function fetchData(){
var items = await APIService.getAllBibleCasts();
// console.log(items);
// console.log(items.data.bibleCasts);
setBibleCastItems([items.data.bibleCasts]);
// items.data.bibleCasts.length > 0 ? setBibleCastItems([items.data.bibleCasts])
// : setBibleCastItems([]);
}
fetchData();
}, []
);
// console.log('bibleCastItems length ' + bibleCastItems.length);
return (
<GridContainer>
<GridItem xs={12} sm={12} md={12}>
<Card plain>
<CardHeader plain color="primary">
<div className={classes.container}>
<div className={classes.left}>
<h4 className={classes.cardTitleWhite}>All BibleCasts</h4>
<p className={classes.cardCategoryWhite}>
Powered by our friends from <b>Unicorn Tech Consultants</b>{" "}
</p>
</div>
</div>
</CardHeader>
<CardBody>
{
bibleCastItems.map((item, index) => <CastItem key={index} bibleCast={item[index]}/>) // this is where I'm facing issue
// bibleCastItems.map((item, index) => {
// console.log(item);
// setMyItem(item);
// return <div key={index}>{index}</div>
// })
}
<div className={classes.right}>
<Fab style={fabStyle} onClick={handleClickOpen}>
<AddIcon />
</Fab>
<UploadFormDialog
open={open}
handleClose={handleClose}
/>
</div>
</CardBody>
</Card>
</GridItem>
</GridContainer>
);
function handleClickOpen(){
setOpen(true);
};
function handleClose(){
setOpen(false);
};
}
Here's my state from browser view:
[![state view][1]][1]
How do I map this state to be a list of components? I'm confused about it
As you can see, I'm using a dialog to create new items and close the dialog once the request is successful. I have one doubt here, how do I tell the main component hosting the dialog that the new data has been fetched and should be added to the state?
My main question here is how to map the items in state to return a list of <CastItem /> component
CastItem Component Snippet
export default function CastItem(props) {
let {bibleCast} = props;
const classes = useStyles();
return <GridContainer>
<GridItem xs={12} sm={6} md={4}>
<Card>
<CardHeader color="info" stats icon>
<CardIcon color="info">
<Streams />
</CardIcon>
</CardHeader>
<CardBody>
<h3 className={classes.cardTitle}>{bibleCast.title}</h3>
<p className={classes.cardCategory}> Reinhard Bonnke</p>
</CardBody>
</Card>
</GridItem>
</GridContainer>
}
CastItem.propTypes = {
bibleCast: PropTypes.object.isRequired,
}
JSON Response from API in console:
[![json response][2]][2]
If you were to create a state variable to represent this response as a list and display that list, how would you go about it, using hooks. Thank you.
[1]: https://i.stack.imgur.com/QkthN.png
[2]: https://i.stack.imgur.com/8Hf11.png
Mistake you are doing is in CreateCast component , form api you are already getting an array again you are passing it inside an array, so it is coming as nested array
Do like this
useEffect(()=>{
async function fetchData(){
var items = await APIService.getAllBibleCasts();
setBibleCastItems(items.data.bibleCasts);
}
fetchData();
}, []
);
For Maping do like this
{
bibleCastItems.map((item, index) => <CastItem key={index} bibleCast={item}/>)
}
// For question how to update parent from child follow below
There are two ways you can set data in a parent component , one is refetch from the api or pass from children to parent and update the state there
I have an example here how to update parent and children,to add names to a list,name list state is maintained in parent component here and child will pass back value to parent by adding name
import { useState } from "react";
import Child from "./Child";
export default function Parent() {
const [list, setList] = useState(["ram"]);
const handleAddName = (name) => {
if (name) {
setList([name, ...list]);
// or you can refetch the list from api here
}
};
return (
<div>
<div style={{ float: "left" }}>
<h1>I am a parent Component</h1>
<ul>
{list &&
list.map((item) => {
return <li key={item}>{item}</li>;
})}
</ul>
</div>
<Child handleSubmit={handleAddName} />
</div>
);
}
Child
import { useState } from "react";
export default function Child(props) {
const [name, setName] = useState("");
const updateNameList = (name) => {
if (name) {
props.handleSubmit(name);
//reset field after data is sent
// you can also save data here making post request respective api
setName("");
}
};
return (
<div style={{ float: "right" }}>
<h1>I am a Child Component</h1>
<p> Add names below</p>
<br />
<input value={name} onChange={(e) => setName(e.target.value)} />
<button onClick={() => updateNameList(name)}>Add</button>
</div>
);
}
refer to this codesand box
What i'm trying to achieve is to have a BaseComponent which will be reuse in different ParentComponent.
My base card component props is ->
export type MSGameCardProps = {
title: string;
fetchGamesFn : (searchText: string) => Promise<IResultObject<any,any>>;
};
My base card render all the necessary basic logics and controls (inputs,autocomplete,title).
For example, it provide an autocomplete which have a simple search debounce functionality.
Me parent component will not necessary have props and will use the base card like so :
export type MSGameSrcCardProps = {};
export const MSGameSrcCard: React.FC<MSGameSrcCardProps> = () => {
const gameSvc = useGameService();
const fetchGame = async (searchText: string) => {
const rs = await result(gameSvc.getAll(searchText));
return rs;
};
return (
<MSGameCard title={"Convert From"} fetchGamesFn={fetchGame}></MSGameCard>
);
};
export default MSGameSrcCard;
The parent component will provide a fetchGames function which can be different.
It will also set the title and may later on set some other flags.
This pattern result with this error : Type '{}' is missing the following properties from type 'MSGameCardProps': title, fetchGamesFn when trying to use the parent component in my page like so : <MSGameSrcCard></MSGameSrcCard>
I don't understand why my parent should have those properties since they are only required in the child component and are fullfill in my parent component function.
I don't want to make them optional(?) since they are actually required; of course only for my base component
I did try to export my basecomponent AS ANY which remove the error but now my props.fetchGamesFn is always undefined even passing it in inside my parent component function.
Maybe i'm doing it wrong but is there a way to have a parent components with no props with child that required props?
EDIT : Here is my MSGameCard base component definition
export const MSGameCard: React.FC<MSGameCardProps> = props => {
const [games, setGames] = React.useState([
{
name: ""
}
]);
const [selectedGame, setSelectedGame] = React.useState<any>();
const [previousGame, setPreviousGame] = React.useState<any>();
const [isLoading, setIsLoading] = React.useState(false);
const [opacity, setOpacity] = React.useState(0);
const fetchGameBase = (searchText: string) => {
setIsLoading(true);
console.log(props.fetchGamesFn);
props.fetchGamesFn(searchText).then(rs =>{
if (rs.isSuccess) setGames(rs.result.data);
setIsLoading(false);
})
};
const searchDebounce = debounce(300, fetchGameBase);
React.useEffect(() => {
fetchGameBase("");
}, []);
const onGameChanged = (event: any, value: any) => {
if (selectedGame) setPreviousGame(selectedGame);
setOpacity(0);
if (value) {
setTimeout(() => {
setSelectedGame(value);
setOpacity(0.2);
}, 300);
}
};
const onInputChanged = (e: any) => {
let value = e.target.value;
if (!value) value = "";
searchDebounce(value);
};
const getSelectedGameImg = () => {
const bgUrl: string = selectedGame
? selectedGame.bg_url
: previousGame?.bg_url;
return bgUrl;
};
return (
<Card style={{ position: "relative", zIndex: 1 }} variant="outlined">
<CardContent style={{ zIndex: 1 }}>
<Typography variant="h5" gutterBottom>
{props.title}
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<Autocomplete
options={games}
getOptionLabel={option => option.name}
onChange={onGameChanged}
onInputChange={onInputChanged}
renderInput={params => (
<TextField
{...params}
label="Source game"
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{isLoading ? (
<CircularProgress color="primary" size={30} />
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
)
}}
/>
)}
/>
</Grid>
</Grid>
<Grid container direction="row" spacing={3}>
<Grid item xs={12} sm={6}>
<TextField label="DPI" fullWidth />
</Grid>
<Grid item xs={12} sm={6}>
<TextField label="Sensitivity" fullWidth />
</Grid>
</Grid>
</CardContent>
<img
style={{ opacity: opacity }}
className="gameImage"
src={getSelectedGameImg()}
/>
</Card>
);
};
export default MSGameCard;
Keep updating notice
After checked the minimum reproducible example you have provided.
I found no type error, am I missing something?
Since the error occurred in both two props, I would leave only the string for the check
import * as React from "react";
import "./styles.css";
export type MSGameCardProps = {
title: string;
};
export type MSGameSrcCardProps = {};
export const MSGameSrcCard: React.SFC<MSGameSrcCardProps> = () => {
return <MSGameCard title={"Convert From"} />;
};
const MSGameCard: React.SFC<MSGameCardProps> = (props: MSGameCardProps) => {
console.log(props); // Object {title: "Convert From"}
return <></>;
};
export default function App() {
return (
<div className="App">
<MSGameSrcCard />
</div>
);
}
Try it online here:
I am using react-hooks to manage a list of JSX.Elements. However, once the element changed, trying to delete it will cause unexpected behavior.
I had tried using useReducer, remove by index etc, still unexpected updated result occurred.
FolderPage.tsx
import React, { useState, useEffect } from 'react';
import { Button, Box, Grid } from 'grommet';
import { Add, Close } from 'grommet-icons';
import { Files } from '../Component/Files/Files';
import { ePub } from '../extension/ePub/ePub';
interface Props {}
export const FolderPage: React.FC<Props> = () => {
const [state, setState] = useState([<Files openFileHandlers={[ePub]} />]);
const newFolderPanel = () => setState(prev => prev.concat(<Files openFileHandlers={[ePub]} />));
const removePanel = (panel: JSX.Element) => setState(prevState => prevState.filter(s => s !== panel));
return (
<div>
<Box align="start" pad="xsmall">
<Button icon={<Add />} label="New Folder Panel" onClick={newFolderPanel} primary />
</Box>
{state.map((s, index) => (
<Grid key={index}>
<Box align="end">
<Button
icon={<Close color="white" style={{ backgroundColor: 'red', borderRadius: '50%', padding: '.25rem' }} />}
type="button"
onClick={() => removePanel(s)}
/>
</Box>
{s}
</Grid>
))}
</div>
);
};
For example, in usage:
What should I change my code so my delete click will delete the matched element?
There is a way to work around it. For each item inside the array. Instead of storing item directly, stored it as {id: yourAssignedNumber, content: item}.
In this way, you could have control of the id, and remove by comparing the id only. This way, it will work correctly.
import React, { useState, useRef } from 'react';
import { Button, Row, Col } from 'antd';
import { Files } from '../Components/Files/Files';
import { fileHandler } from '../model/fileHandler';
interface Props {
fileHandlers?: fileHandler[];
}
export const FolderPage: React.FC<Props> = ({ fileHandlers }) => {
const [state, setState] = useState([{ key: -1, content: <Files fileHandlers={fileHandlers} /> }]);
const key = useRef(0);
const newFolderPanel = () =>
setState(prev =>
prev.concat({
key: key.current++,
content: <Files fileHandlers={fileHandlers} />
})
);
const removePanel = (key: number) => setState(prevState => prevState.filter(s => s.key !== key));
return (
<Row>
<Button type="primary" icon="plus" onClick={newFolderPanel} style={{ margin: '.75rem' }}>
New Foldr Panel
</Button>
{state.map(({ key, content }) => (
<Col key={key}>
<div
style={{
background: '#75ff8133',
display: 'grid',
justifyItems: 'end',
padding: '.5rem 1rem'
}}>
<Button onClick={() => removePanel(key)} icon="close" type="danger" shape="circle" />
</div>
{content}
</Col>
))}
</Row>
);
};