I am facing an issue with having my res value (array) from the getLogs function to be populated to the DataGrid.
UPDATED CODES:
LogDetails.jsx
const columns = [
{ field: "id", headerName: "ID", width: 30 },
{
field: "name",
headerName: "Name",
width: 250,
},
{
field: "activity",
headerName: "Activity",
width: 350,
},
{
field: "date",
headerName: "Date",
width: 250,
},
];
export default function LogDetails() {
const [logs, setLogs] = useState([]);
useEffect(() => {
function logs() {
getLogs().then((res) => {
setLogs(res);
});
}
logs();
}, []);
return (
<Box sx={{ width: "100%" }}>
{logs.length ? (
<DataGrid
rows={logs}
columns={columns}
pageSize={10}
rowsPerPageOptions={[10]}
disableSelectionOnClick
autoHeight
/>
): null}
</Box>
);
}
function.js
export async function getLogs() {
var rows = [];
const q = query(collection(db, "logs"), orderBy("date", "desc"));
const docQueury = await getDocs(q);
var count = 1;
docQueury.forEach(async (log) => {
const useref = await getDoc(log.data().user);
const date = new Timestamp(
log.data().date.seconds,
log.data().date.nanoseconds
)
.toDate()
.toLocaleString("en-sg");
rows.push({
id: count++,
name: useref.data().name,
activity: log.data().activity,
date: date,
});
});
return rows;
}
Output of "rows" from getLogs function:
Output of states from LogDetails.jsx:
UPDATE:
If I were to run the above codes and then delete what is under useEffect(), the data will be populated.
useEffect(() => {
//delete what is under here
}, []);
In addition, I happened to experiment using const rows in the page itself. The data was able to populate successfully to the grid. Thus, right now I suppose it has to do with how my codes under useEffect() has some issue?
const rows = [
{
id: 1,
name: "test",
activity: "test",
date: "test"
}
]
The correct way to do the conditional render of the DataGrid would be:
return (
<Box sx={{ width: "100%" }}>
{fetchLogs.length ? (
<DataGrid
rows={fetchLogs}
columns={columns}
pageSize={10}
rowsPerPageOptions={[10]}
disableSelectionOnClick
autoHeight
/>
): null}
</Box>
);
fetchLogs && (<DataGrid/>) will always render the DataGrid because an empty array is "truthy". And fetchLogs.length && (<DataGrid/>) would render 0 when the array is empty.
I'm not sure if this solves the OP's problem but at least it would yield the expected results with regard to conditionally rendering the grid. And as per my previous comment, fetchLogs is not a good name for a state variable. It sounds like a function.
Since you are fetching the data from an api, in the beginning the fetchLogs will be undefined.
So first you should check if fetchLogs exists and only then populate to the DataGrid.
Update
As you see, you already get the data in getLogs function, you could do this instead. Now, if you console the logs, data should be there. And if still the table is not populated, it means field names don't match.
Also pay attention to naming the functions and variables. Make them more readable.
useEffect(() => {
const data = getLogs()
setLogs(data)
}, []);
return (
<Box sx={{ width: "100%" }}>
{fetchLogs.length>0 && (
<DataGrid
rows={fetchLogs}
columns={columns}
pageSize={10}
rowsPerPageOptions={[10]}
disableSelectionOnClick
autoHeight
/>
)}
</Box>
);
Related
I'm building an app with many tables. Generally they look the same, but some has different columns set or cell content. I'd like to make a reusable table component which can be used through out the entire app.
At the moment I made a table wrapper component which accepts children and table type. Columns are rendered depending on table type. Then I have several components each for different table because data structure may differ and layout might be slightly different. The question is how to make a single table component which can handle different data structures and different layouts?
I think it can be done with conditional rendering (but I do not want excessively use this because it will be hard to maintain and difficult to read):
{tableType === "first" ? <TableCell>{item.name}</TableCell> : null}
{tableType === "second" ? <TableCell>{item.status}</TableCell> : null}
I was told that it can be done in somekind of this way:
<TableCell>{getCellComponent(tableType, value)}</TableCell>
Unfortunatelly I'm not that smart enough to make it myself. I understand the general idea for this approach but don't understand how to make it. Can someone help me with that please?
I made a Codesandbox with two simplified tables:
https://codesandbox.io/s/complex-table-yrrv6c?file=/src/App.tsx
UPD
I went with #Dilshan solution. It works great. Meanwhile there're couple TS errors which I don't know how to fix.
I want to store columns props in a variable. Like this:
const columnObj = {
firstName: {
name: "First Name",
width: "25%",
accessor: (payload: any) => (
<>
<Avatar />
{payload.who.nickname}
</>
)
},
// ...
};
But then what type should I specify for payload?
I'd like to pass onClick handler to the table. Basically I want whole row to be clickable:
<MyTable<PayloadTyep2>
columns={columnObj}
payload={secondTableData}
onClick={(id) => console.log("Row clicked:", id)}
/>
Then in assign it in Table component:
<TableBody>
{payload.map((rowData, index) => {
return (
<TableRow key={index}
onClick={(id) => props.onClick(rowData.id)}> // here get TS error
// ...
</TableRow>
);
})}
</TableBody>
How to fix that?
Here's forked Codesanbox: https://codesandbox.io/s/react-mui-reusable-table-forked-vk7h6o?file=/src/App.tsx
Lets think about your requirement. You need to render a table based on given data payload. Table needs to know what's it columns. We can provide columns as a n input as well,
<MyTable
data={myData}
columns=['col1', 'col2', 'col3']
/>
We can assume col1, col2, col3 are keys of the myData object so the MyTable components can now extract the cell data by simply doing myData[columns[i]] where i is the index of column array item.
Based on your mock data, I can see there are nested objects in your data as well. Therefore simple myData[columns[i]] is not going to work. As a solution we can provide a function to return cell value in the component props.
type TableProps<T extends object> = {
columns: Record<
string,
{
name: string;
width: string;
accessor: (data: T) => ReactNode | string | undefined;
}
>;
payload: T[];
};
export const MyTable = <T extends object>(props: TableProps<T>) => {}
As you can see accessor is a function which has one argument type T which returns a React element or string or nothing.
Now in the table component we can simply do,
<TableRow key={index}>
{Object.keys(columns).map((key) => {
const { accessor } = columns[key];
return (
<TableCell key={key} align="left">
{accessor(rowData)}
</TableCell>
);
})}
</TableRow>
Then when you use the Table component,
<TableWithTitle<PayloadType1>
columns={{
phone: {
name: "Phone",
width: "14%",
accessor: (payload) => payload.phohe
},
notes: {
name: "Notes",
width: "14%",
accessor: (payload) => {
return payload.notes?.map(({ note }) => note).join(", ");
}
}
}}
....
Here is a full code sample
The basic idea is to make code less fragile to changes, i.e everytime you add a new table type or make changes to existing table, the affect on other table types should be minimum, you can read more about SOLID principles later.
use composition to make component more reusable
There is a basic idea of the solution
// create a factory/config to pick the right header columns based on type
const tableOneColumnHeaders = [
{ id: 1, name: "First name", width: "25%" },
{ id: 2, name: "Second name", width: "16%" },
{ id: 3, name: "Address", width: "14%" },
{ id: 4, name: "Phone", width: "14%" },
{ id: 5, name: "Notes", width: "14%" }
];
const tableTwoColumnHeaders = [
{ id: 1, name: "First name", width: "25%" },
{ id: 2, name: "Status", width: "16%" },
{ id: 3, name: "Author", width: "14%" },
{ id: 4, name: "Date", width: "14%" },
{ id: 5, name: "Media", width: "14%" },
{ id: 6, name: "Rating", width: "14%" },
{ id: 7, name: "Project", width: "14%" },
{ id: 8, name: "", width: "3%" }
];
// poor mans factory
const headerColumnsFactory: headerType = {
[TableType.FirstTable]: tableOneColumnHeaders,
[TableType.SecondTable]: tableTwoColumnHeaders
};
// create a row renderer factory/config to pick the right renderer
// each table has a custom body renderer
const TableOneRowsMapper = (props: { data: RowData[] }) => {
const { data } = props;
const rows = data as FirtTableDataType[];
return (
<>
{rows?.map((item) => (
<TableRow key={item.id}>
<TableCell component="th" scope="row">
{item.name}
</TableCell>
<TableCell align="left">{item.address}</TableCell>
...
const TableTwoRowsMapper = (props: { data: RowData[] }) => {
const { data } = props;
const rows = data as SecondTableDataType[];
return (
<>
{rows.map((item) => (
<TableRow key={item.id}>
<TableCell
sx={{
display: "flex",
direction: "row",
gap: "5px",
alignItems: "center"
}}
>
<Avatar />
{item.who.nickname}
...
const TableBodyRowsComponentFactory = {
[TableType.FirstTable]: TableOneRowsMapper,
[TableType.SecondTable]: TableTwoRowsMapper
};
/
/ A component that uses the factories to pick the right renders and render the table
const ExtensibleTable = (props: {
title: string;
type: TableType;
data: any[];
}) => {
const { title, type, data } = props;
// if a switch of if is used, this code becomes fragile
/*
// with introduction of new if else or modification of existing if
// othe tables types can break because of shared variable etc
if (type === '') {
return some columsn
} else if ( type === 'xy') {
}
*/
// but with a bulder the right components are picked
// and changes to each type of component are seperated
// new ones can be added without affecting this common code
// pick the right header columns
const headerColumns: HeaderRowType[] = React.useMemo(
() => headerColumnsFactory[type] ?? [],
[type]
);
// pick the right row renderer
const RowRenderer = React.useMemo(
() => TableBodyRowsComponentFactory[type] ?? TableEmptyRenderer,
[type]
);
return (
<BaseTable
title={title}
headerRow={
<TableRow>
{headerColumns.map(({ name, id, width }) => (
<TableCell align="left" width={width} key={id}>
{name}
</TableCell>
))}
</TableRow>
}
>
<RowRenderer data={data} />
</BaseTable>
);
};
const BaseTable = (props: IBaseTableProps) => {
const { title, children, headerRow } = props;
return (
<Stack
gap={"20px"}
alignItems={"center"}
sx={{ background: "lightblue", padding: "20px", borderRadius: "20px" }}
>
<Typography variant="h3">{title}</Typography>
<Table>
<TableHead>{headerRow}</TableHead>
<TableBody>{children}</TableBody>
</Table>
</Stack>
);
};
I had created a codesandbox example with rest of the example
generally more usable the component becomes, less flexible it becomes
to reduce/handle such issues it helps to apply SOLID principles like Inversion of Control
I am not used to Typescript but I hope helps you in someway and gives you an general idea to make reusable compnents
You can learn and refer to the "antd" Table Components.The component defined a amount of params to show different format content includes text,icon,image,tree even a table.
I give you an example for your reference:
First you may define columnList object:
let columnList = [
{ label: "Post", accessor: "post" },
{ label: "Name", accessor: "name" },
{ label: "Email", accessor: "email" },
{ label: "Primary Phone No", accessor: "primaryPhoneNo" },
{ label: "Secondary Phone No", accessor: "secondaryPhoneNo" }
]
<DataTable
columnList={columnList}
dataList={staffList}
......../>
where
dataList is an array of data,
the label field of the columnList object is store the column label,
and the accessor is the field name in dataList.
In DataTable component,
export default function DataTable({
columnList,
dataList
}) {
return(
<table >
<thead>
<tr> {
columnList.map((column, index) => (
<th> {
column.label
}
</th>
))
}
</tr>
</thead>
<tbody> {
dataList.map((data) => (
<tr >
columnList.map((column, colIindex) => (
<td > {
data[column.accessor]
} </td>
))
</tr>
));
}
</tbody>
</table>
)
}
I have 3 dropdowns, each which controls state. When this dropdown is selected it will set the target and send it to redux. For example.
const [interviewStep1, setinterviewStep1] = useState('Phone Screening')
const [interviewStep2, setinterviewStep2] = useState('Formal Interview')
const [interviewStep3, setinterviewStep3] = useState('Reference Check')
This is sent to redux in this manner.
<Dropdown_Content>
{interviewStageSelection.map((option) => (
<Dropdown_Item
key={option}
onClick={(e) => {
setinterviewStep1(option)
setisActive(!isActive)
console.log(interviewStep1)
setisActive(false)
updateInterview1(dispatch, option)
}}
>
<Typography variant="subtitle5" color="black" sx={{ "&:hover": { color: "white" } }}>{option}</Typography>
</Dropdown_Item>
))}
</Dropdown_Content>
I then pass this state as props into my next component.
export default function JobPostInterviewVerticalStepper(interviewStep1, interviewStep2, interviewStep3)
this does come through, but then I want to display in my array. How do I use these props?
const steps = [
{
label: 'Phone Screening',
//I WANT A interviewStep1 here!
},
{
label: 'Formal Interview',
},
{
label: 'Reference Check',
},
{
label: 'Offer',
},
];
I'm new in reactJS I stack in this problem :
I have to fill the table with data from API
the problem: I can fill the table only with the const variable, not in the useEffect function.
the code:
function CandidaturesList() {
const [candidatures , setCandidatures]=useState(null);
useEffect(() => {
async function fetchData() {
try {
const response = /*some code to get data*/
setCandidatures(response);
} catch (error) {
// Catch any errors for any of the above operations.
alert(
`Failed to load `,
);
console.error(error);
}
}
fetchData();
},[candidatures]);
// project table start
const project = [
{
title: "COMPANIES",
dataIndex: "name",
width: "32%",
},
{
title: "JOB TITLE",
dataIndex: "jobtitle",
},
{
title: "DECISION",
dataIndex: "decision",
},
];
const candidature = [
{
key: "1",
name: (
<>
<div className="avatar-info">
<Title level={5}>Spotify Version</Title>
</div>
</>
),
jobtitle: (
<>
<div className="semibold">Manager</div>
</>
),
decision: (
<>
<div className="ant-progress-project">
<Tag icon={<CheckCircleOutlined />} color="#87d068">
Valid
</Tag>
</div>
</>
),
},
{
key: "2",
name: (
<>
<div className="avatar-info">
<Title level={5}>Progress Track</Title>
</div>
</>
),
jobtitle: (
<>
<div className="semibold">Developer full stack</div>
</>
),
decision: (
<>
<div className="ant-progress-project">
<Tag icon={<SyncOutlined spin />} color="#108ee9">
Processing
</Tag>
</div>
</>
),
},
]
return (
<div className="table-responsive">
<Table
columns={project}
dataSource={candidature}
pagination={false}
className="ant-border-space"
/>
</div>
);}
So the const candidature is just having a fake data
and the candidatures state is having the real data to put in the table
the question is: how can I map the "candidatures" state to put the data in the table with the same style as the "const candidature" or how can I put the data of "candidatures" in this const?
One thing to note here is that candidatures state variable is array of objects, like
{[titre_poste: 'developeur full stack', nomCompagnie: 'nomCompagnie',decision: "none",id: "2"],[ ... ] }```
Firstly, i see that candidatures is your data and projects is your table schema
you shouldn't put html within your data array, you should instead use the render key within your schema array which in your case is projects.
it should look something like this
// project table start
const project = [
{
title: "COMPANIES",
dataIndex: "name",
width: "32%",
render: text => (
<div className="avatar-info">
<Title level={5}>{text}</Title>
</div>
)
}
]
const candidature = [
{
key: "1",
name: "Some string value"
}
]
secondly, in your useEffect function, you have created a recursion via your dependency of candidatures.
You are listening for changes in candidatures while upon every change you trigger an api call which changes the candidatures state which in turn again triggers the useEffect call.
to prevent this, remove candidatures as useEffect dependency.
We are using MUI DataGrid in our React based project.
At the moment I am trying to save/persist state of columns after toggling visibility of some columns with DataGrid's toolbar column menu, as currently after re-render it is back to default column setup.
I would like to know how could I access state of DataGrid/visibility state of columns in DataGrid so I can adjust/save it/reuse it later?
So far I meddled a bit with a apiRef, but all I got from apiRef.current was empty object. I am adding below some basic codeSandbox example to show how I tried to access it.
https://codesandbox.io/s/datagridprodemo-material-demo-forked-189j9?file=/demo.js
Maybe there is better/different approach, or I just need to create the state somehow. We would like to persist the state of the columns as user preference possibly in a future so this is vital for that to happen.
All suggestions are welcome and I thank you beforehand.
Fortunately, the DataGrid API provides the columnVisibilityModel and onColumnVisibilityChange props.
See this code sandbox for a simple example of controlling the columnVisibilityModel: https://codesandbox.io/s/mui-datagrid-oncolumnvisibilitychange-savestate-u1opzc?file=/src/App.tsx:1960-1984
Here is the code for a simple implementation. Your initial state may vary. Also, note that I could not figure out how to get DataGridPro to call onColumnVisibilityChange unless columnVisibilityModel was initially undefined. Bug, or my mistake, I am uncertain.
import "./styles.css";
import React from "react";
import {
DataGrid,
GridRowsProp,
GridColDef,
GridCallbackDetails,
MuiEvent,
GridColumnVisibilityModel,
GridColumnVisibilityChangeParams
} from "#mui/x-data-grid";
import { Button } from "#mui/material";
const rows: GridRowsProp = [
{ id: 1, col1: "Hello", col2: "World" },
{ id: 2, col1: "DataGridPro", col2: "is Awesome" },
{ id: 3, col1: "MUI", col2: "is Amazing" }
];
const columns: GridColDef[] = [
{ field: "col1", headerName: "Column 1", width: 150 },
{ field: "col2", headerName: "Column 2", width: 150 }
];
const initialVisibilityModel = { col1: true, col2: true };
export default function App() {
// it is strange, but in order for DataGridPro to call onColumnVisibilityChange, columnVisibilityModel must be undefined initially
const [
currentGridColumnVisibilityModel,
setCurrentGridColumnVisibilityModel
] = React.useState<GridColumnVisibilityModel | undefined>(undefined);
const [mySavedValue, setMySavedValue] = React.useState<
GridColumnVisibilityModel | undefined
>(undefined);
const onColumnVisibilityChange = React.useCallback(
(
params: GridColumnVisibilityChangeParams,
event: MuiEvent<{}>,
details: GridCallbackDetails
): void => {
console.log("params", params);
setCurrentGridColumnVisibilityModel((s) => ({
// per the DataGridPro strangeness, we must marry in initial state only the first update
...(s ? s : initialVisibilityModel),
[params.field]: params.isVisible
}));
},
[]
);
const saveACopyOfGridState = () => {
setMySavedValue(currentGridColumnVisibilityModel || initialVisibilityModel);
};
const loadSavedCopyOfGridState = () => {
setCurrentGridColumnVisibilityModel(mySavedValue || initialVisibilityModel);
};
const currentVisibilityAsText =
`${Object.keys(currentGridColumnVisibilityModel ?? {}).map(
(key) => `{${key}:${currentGridColumnVisibilityModel?.[key]}}`
)}` || "empty";
const savedVisibilityAsText =
`${Object.keys(mySavedValue ?? {}).map(
(key) => `{${key}:${mySavedValue?.[key]}}`
)}` || "empty";
return (
<div style={{ height: 300, width: "100%" }}>
<DataGrid
rows={rows}
columns={columns}
columnVisibilityModel={currentGridColumnVisibilityModel}
onColumnVisibilityChange={onColumnVisibilityChange}
/>
<div>
<Button onClick={saveACopyOfGridState} variant="contained">
SAVE CURRENT COLUMN VISIBILITY STATE
</Button>
<Button
onClick={loadSavedCopyOfGridState}
variant="contained"
color="warning"
>
LOAD SAVED COLUMN VISIBILITY STATE
</Button>
</div>
<p>Current filter state: {currentVisibilityAsText}</p>
<p>Saved filter state: {savedVisibilityAsText}</p>
</div>
);
}
I made a hook to persist column settings to localStorage. It uses callbacks on the API ref object. Usage:
function MyGrid() {
const apiRef = useGridApiRef()
usePersistColumnSettings(apiRef, 'customers-grid')
return <DataGrid apiRef={apiRef} />
}
I am pulling down results from an API, like so:
const [state, setState] = React.useState({
matches: undefined,
chosenBets: [{}]
});
const API = "https://api.myjson.com/bins/i461t"
const fetchData = async (endpoint, callback) => {
const response = await fetch(endpoint);
const json = await response.json();
setState({ matches: json });
};
And rendering JSX based off it using the map() function:
export function MatchCardGroup(props) {
return (
<div>
{props.matches.map((match, i) => {
return (
<MatchCard
key={i}
matchCardIndex={i}
team_home={match.teams[0]}
team_away={match.teams[1]}
league_name={match.sport_nice}
odd_home={match.sites[0].odds.h2h[0]}
odd_draw={match.sites[0].odds.h2h[1]}
odd_away={match.sites[0].odds.h2h[2]}
onClick={props.onClick}
timestamp={match.timestamp}
/>
);
})}
</div>
);
}
I then have a card which has odds on it, each odd with its own click event:
export function MatchCard(props) {
const [state, setState] = React.useState({
selection: {
id: undefined
}
});
const {
timestamp,
team_home,
team_away,
league_name,
odd_away,
odd_draw,
odd_home,
onClick,
matchCardIndex,
selection
} = props;
const odds = [
{
id: 0,
label: 1,
odd: odd_home || 1.6
},
{
id: 1,
label: "X",
odd: odd_draw || 1.9
},
{
id: 2,
label: 2,
odd: odd_away || 2.6
}
];
const handleOnClick = (odd, oddIndex) => {
// need to changhe the selection to prop
if (state.selection.id === oddIndex) {
setState({
selection: {
id: undefined
}
});
onClick({}, matchCardIndex);
} else {
setState({
selection: {
...odd,
team_home,
team_away
}
});
onClick({ ...odd, oddIndex, team_home, team_away, matchCardIndex });
}
};
React.useEffect(() => {}, [state, props]);
return (
<div style={{ width: "100%", height: 140, backgroundColor: colour.white }}>
<div>
<span
style={{
...type.smallBold,
color: colour.betpawaGreen
}}
>
{timestamp}
</span>
<h2 style={{ ...type.medium, ...typography }}>{team_home}</h2>
<h2 style={{ ...type.medium, ...typography }}>{team_away}</h2>
<span
style={{
...type.small,
color: colour.silver,
...typography
}}
>
{league_name}
</span>
</div>
<div style={{ display: "flex" }}>
{odds.map((odd, oddIndex) => {
return (
<OddButton
key={oddIndex}
oddBackgroundColor={getBackgroundColour(
state.selection.id,
oddIndex,
colour.lime,
colour.betpawaGreen
)}
labelBackgroundColor={getBackgroundColour(
state.selection.id,
oddIndex,
colour.lightLime,
colour.darkBetpawaGreen
)}
width={"calc(33.3% - 8px)"}
label={`${odd.label}`}
odd={`${odd.odd}`}
onClick={() => handleOnClick(odd, oddIndex)}
/>
);
})}
</div>
</div>
);
}
In my App Component I am logging the returned object from the click event:
const onClick = obj => {
// check if obj exists in state.chosenBets
// if it exists, remove from array
// if it does not exist, add it to the array
if (state.chosenBets.filter(value => value == obj).length > 0) {
console.log("5 found.");
} else {
console.log(state.chosenBets, "state.chosenBets");
}
};
And what I want to do is this:
When the user clicks an odd of any given match, add that odd to chosenBets
If the user deselects the odd, remove that odd from chosenBets
Only 1 odd from each of the 3 possible odds of any match can be selected at any time
Bonus points: the selected odd is selected based on the global state from App, instead of local state. This is so if I edit the array elsewhere, it should update in the UI.
Any help would be greatly appreciated, I'm lost here!
Link to Codesandbox
I've taken a short look at your project, and here are a few pointers to help you out:
Objects are only equal by reference.
This means that
{ id: 0, matchCardIndex: 8 } === { id: 0, matchCardIndex: 8 }
is false, even if you expect it to be true. To compare them, you need to compare every key in the object:
value.id === obj.id && value.matchCardIndex === obj.matchCardIndex
This also affects the filter call you have in the index.tsx, so you should change the comparison there to something similar to
state.chosenBets.filter(value => value.id === obj.id && value.matchCardIndex === obj.matchCardIndex)
State should only live in one place
As you already mentioned, it would be better to keep the state in your index.tsx if it also you needed there, and don't keep it locally in the components further down the tree. I'd suggest having the components only render the state, and have handlers to change the state.
Example
Here's a fork of your code sandbox I think implements it in a way that you described: https://codesandbox.io/s/gifted-star-wg629-so-pg5gx