So basically I'm trying to use dnd in a simple game, but i feel like i don't quite understand the point of some params. I have two components being used as a source and target. The source is receiving a unit prop from the parent and i'd like the Field component(target) to know which unit is being dragged atm and after the drop display it inside it. I've seen i can specify some fields in beginDrag(), but i think it may not be it. Is is possible or is there a workaround?
const BenchTileSource = {
beginDrag(props) {
// Return the data describing the dragged item
const item = { unit: props.unit };
return item;
}
};
const TileBackground = styled.div`
...
`;
function UnitBenchTile({ unit }) {
const [{ isDragging }, drag] = useDrag({
item: { type: TileTypes.BENCH_TILE },
collect: monitor => ({
isDragging: !!monitor.isDragging()
})
});
return unit ? (
<TileBackground ref={drag}>{unit.name}</TileBackground>
) : (
<TileBackground />
);
}
export default BenchTileSource(
TileTypes.BENCH_TILE,
BenchTileSource
)(UnitBenchTile);
import React from "react";
import styled from "styled-components";
import { connect } from "react-redux";
import { TileTypes } from "../dragTypes/TileTypes";
import { useDrop } from "react-dnd";
const mapStateToProps = function(state) {
return {
currentPosition: state.currentPosition,
unitsPositions: state.unitsPositions
};
};
...
function Field({ index, children, dispatch }) {
const unitsPositions = useSelector(
state => state.unitsPositionsReducer.unitsPositions
);
const [{ isOver }, drop] = useDrop({
accept: TileTypes.BENCH_TILE,
drop: monitor => setCurrentPosition([x, y], monitor.getItem()),
collect: monitor => ({
isOver: !!monitor.isOver(),
item: monitor.item
})
});
const setCurrentPosition = (newPosition, unit) => {
if (!unitsPositions[index].unit) {
const result = unitsPositions;
result[index] = { position: newPosition, unit: unit };
dispatch({ type: "UPDATE_UNITS_POSITIONS", result });
}
};
const isRight = (index - 9) % 10 === 0;
const isBottom = index >= 50;
const x = index % 10;
const y = Math.floor(index / 6);
if (isRight && isBottom)
return <FrameBottomRight ref={drop}>{children}</FrameBottomRight>;
else if (isRight) return <FrameRight ref={drop}>{children}</FrameRight>;
else if (isBottom) return <FrameBottom ref={drop}>{children}</FrameBottom>;
else return <Frame ref={drop}>{children}</Frame>;
}
export default connect(mapStateToProps)(Field);
All the information that is needed can be accessed by the monitors of react-dnd. In this case, you want to access the currently dragged source in the target so you have to make use of the DropTargetMonitor's method getItem() and it will return all the properties defined in the item property of the dragged source. No need to supply additional fields when a drag operation begins if you already have an id defined in your item, that field is enough to identify which item is being dragged.
UPDATE
if you really want to pass additional fields, you can simply merge the item data by "spreading" it to a new object together with your additional data in your begin method.
return {
...monitor.item,
additional: data
}
Related
I'm trying to work on a react page that has 3 states: View: 0, Edit and Add
the page state keeps track of current record id and the state page is in. Here are the rules.
record id can change only if page is in view mode
if page enters Edit/Add mode then unless user is out of that mode, record id must not change and the page state stays as it is
here is my code to achieve the same
interface IPageManager{
id: number,
mode: Modes,
grid: {page: number, size: number},
setId(id: number): void,
enterAddMode(): void,
enterEditMode(teamId: number): void,
enterViewMode(id?: number): void
debug(msg: string):void
}
type PageStateType = {
grid: {page: number, size: number},
id: number,
mode: Modes
}
function usePageManager(): IPageManager {
const [state, setState] = useState({ grid: {page: 1, size: 10}, id: 0, mode: Modes.View } as PageStateType)
const isEditing = () => state.mode === Modes.Add || state.mode === Modes.Edit
const change = (teamId?: number, mode: Modes= Modes.View)=>{
if(!isEditing())
setState(current=>produce(current, x=>{
if(teamId)
x.id = teamId
x.mode = mode
}))
}
const manager: IPageManager = {
get id() { return state.id }
, get mode() { return state.mode }
, get grid() { return state.grid }
, setId(id: number) {
setState(previous => produce(previous, x => { x.id = id }))
}
, enterAddMode() { change(undefined, Modes.Add) }
, enterEditMode(teamId: number) { change(teamId, Modes.Edit) }
, enterViewMode(id?: number){
if(!isEditing()){
setState(previous=>produce(previous, x=>{
x.mode = Modes.View
if(id)
x.id = id as number
}))
}
}
, debug(msg: string){
const mode = state.mode===Modes.View? 'View': state.mode===Modes.Edit? 'Edit':'Add'
trace(`${msg} - {id:${state.id}, mode:${mode}}`)
}
}
return manager
}
export function TeamsPage() {
const header = useMemo(() => <PageHeader title="Teams Management" />, [])
const manager = usePageManager()
manager.debug('page') // reflects correct state
const { data }= useQuery('teams', async () => await getTeams(manager.grid.page, manager.grid.size))
const view =(id: number)=>{
manager.debug(`before`) // manager state always at initial state
if(manager.mode===Modes.View) //always true since default is View
{
manager.setId(id)
manager.debug(`after`) // manager state always at initial state
}
}
const listing = useMemo(() => {
const { rows, count } = data ?? { rows: [], count: 0 } as TeamSummary
const t0 = (
<Listing
data={rows}
page={manager.grid.page}
size={manager.grid.size}
count={count}
onSelect={(arg) => view(arg.id) }
onAdd={() => manager.enterAddMode()}
onEdit={(arg) => manager.enterEditMode(arg.id)}
/>)
return t0
}, [data])
useEffect(() => { // if the data is updated, set the recordId to the first record in grid
const { rows, count } = data ?? { rows: [], count: 0 } as TeamSummary
if (rows && rows.length>0) {
const first = _.first(rows)
if (first)
manager.setId(first.id)
}
}, [data])
// it renders either an editor or a viewer, does not mantain any state on its own. depends on props only
const detail =<RecordBar id={manager.id} mode={manager.mode} getRecord={getTeam} emptyRecord={() => emptyTeamDetailResponse} />
return (
<Layout
summary={listing}
detail={detail}
header={header}
footer={<></>}
/>
)
}
In my opinion the page should work correctly but this is what I see in output
As you can see, after entering the Edit mode, page does change the current id despite the fact that if condition checks if the page is in view state or not before making any changes. To that if condition, state is always initial state even though in page it is reflected correctly.
is it some sort of closure problem or react issue or my coding is incorrect?
is it the correct way to manage the view i.e. by creating a custom hook that holds the state and returns the functions to operate on state?
I have a component that I want to show a graph with multiple line series representing price changes over the last 24 hours. I have an endpoint that sends this data and I use the code below to show it.
One of the issues comes from errors seeming to come from the library itself meaning the graph will not even show up. Errors from the console when I load the page.
Other times, the page will load for a second and then go white and drain enough CPU to cause a crash.
The few times that the graph actually shows up on screen, it does not show any lines until the lines 81-85 are uncommented which it then shows the lines but does not zoom in on them leaving a mess on the screen.
Any help would be much appreciated.
/* eslint-disable new-cap */
/* eslint-disable #typescript-eslint/no-unused-vars */
/* eslint-disable no-magic-numbers */
import React, { useEffect, useState } from "react";
import { LegendBoxBuilders, lightningChart, Themes } from "#arction/lcjs";
import "./TopCurrencyGraph.css";
import axios from "axios";
export interface data {
data: dataPoint[];
}
export interface dataPoint {
currency: string;
percentage: number;
timestamp: string;
}
interface graphPoint {
x: number;
y: number;
}
const TopCurrencyGraph = () => {
const historicalAddr = `http://${
process.env.back || "localhost:8000"
}/historical24hChangeData`;
useEffect(() => {
const map: { [name: string]: graphPoint[] } = {};
axios
.get(historicalAddr)
.then((res) => {
const { points } = res.data;
const pointList = points as dataPoint[];
pointList.forEach((obj) => {
const newPoint = {
x: new Date(obj.timestamp).getTime() * (60 * 24),
y: obj.percentage * 100,
};
if (obj.currency in map) {
map[obj.currency].push(newPoint);
} else {
map[obj.currency] = [newPoint];
}
});
})
.catch((err) => {
console.log(err, historicalAddr);
});
const chart = lightningChart().ChartXY({
theme: Themes.lightNew,
container: "currency-graph",
});
chart.setTitle("Top Currencies");
chart.getDefaultAxisX().setTitle("Time");
chart.getDefaultAxisY().setTitle("Percentage Change");
const entries = Object.entries(map);
const names = entries.map(([a, _b]) => a);
const lists = entries.map(([_, b]) => b);
const seriesArray = new Array(5).fill(null).map((_, idx) =>
chart
.addLineSeries({
dataPattern: {
pattern: "ProgressiveX",
},
})
// eslint-disable-next-line arrow-parens
.setStrokeStyle((stroke) => stroke.setThickness(1))
.setName(names[idx])
);
seriesArray.forEach((series, idx) => {
if (idx === 1) {
series.add(lists[idx]);
}
});
chart.addLegendBox(LegendBoxBuilders.HorizontalLegendBox).add(chart);
return () => {
chart.dispose();
};
}, []);
// done thnx
return (
<div className="graph-container">
<div id="currency-graph" className="graph-container"></div>
</div>
);
};
export default TopCurrencyGraph;
Your code looks syntax wise correct, but I believe you are running into issues due to not managing asynchronous code (axios getting data from your endpoint) properly.
const map: { [name: string]: graphPoint[] } = {};
axios
.get(historicalAddr)
.then((res) => {
// This code is NOT executed immediately, but only after some time later.
...
})
// This code and everything below is executed BEFORE the code inside `then` block.
// Because of this, you end up supplying `undefined` or other incorrect values to series / charts which shows as errors.
const chart = lightningChart().ChartXY({
theme: Themes.lightNew,
container: "currency-graph",
});
You might find it useful to debug the values you supply to series, for example like below. I think the values are not what you would expect.
seriesArray.forEach((series, idx) => {
if (idx === 1) {
console.log('series.add', lists[idx])
series.add(lists[idx]);
}
});
Improvement suggestion
Here's my attempt at modifying the code you supplied to manage the asynchronous data loading correctly, by moving all code that relies on the data after the data is processed.
/* eslint-disable new-cap */
/* eslint-disable #typescript-eslint/no-unused-vars */
/* eslint-disable no-magic-numbers */
import React, { useEffect, useState } from "react";
import { LegendBoxBuilders, lightningChart, Themes } from "#arction/lcjs";
import "./TopCurrencyGraph.css";
import axios from "axios";
export interface data {
data: dataPoint[];
}
export interface dataPoint {
currency: string;
percentage: number;
timestamp: string;
}
interface graphPoint {
x: number;
y: number;
}
const TopCurrencyGraph = () => {
const historicalAddr = `http://${
process.env.back || "localhost:8000"
}/historical24hChangeData`;
useEffect(() => {
const chart = lightningChart().ChartXY({
theme: Themes.lightNew,
container: "currency-graph",
});
chart.setTitle("Top Currencies");
chart.getDefaultAxisX().setTitle("Time");
chart.getDefaultAxisY().setTitle("Percentage Change");
const seriesArray = new Array(5).fill(null).map((_, idx) =>
chart
.addLineSeries({
dataPattern: {
pattern: "ProgressiveX",
},
})
// eslint-disable-next-line arrow-parens
.setStrokeStyle((stroke) => stroke.setThickness(1))
);
chart.addLegendBox(LegendBoxBuilders.HorizontalLegendBox).add(chart);
axios
.get(historicalAddr)
.then((res) => {
const { points } = res.data;
const pointList = points as dataPoint[];
const map: { [name: string]: graphPoint[] } = {};
pointList.forEach((obj) => {
const newPoint = {
x: new Date(obj.timestamp).getTime() * (60 * 24),
y: obj.percentage * 100,
};
if (obj.currency in map) {
map[obj.currency].push(newPoint);
} else {
map[obj.currency] = [newPoint];
}
});
const entries = Object.entries(map);
const names = entries.map(([a, _b]) => a);
const lists = entries.map(([_, b]) => b);
seriesArray.forEach((series, idx) => {
series.setName(names[idx])
if (idx === 1) {
series.add(lists[idx]);
}
});
})
.catch((err) => {
console.log(err, historicalAddr);
});
return () => {
chart.dispose();
};
}, []);
// done thnx
return (
<div className="graph-container">
<div id="currency-graph" className="graph-container"></div>
</div>
);
};
export default TopCurrencyGraph;
I am using React, Typescript and Apollo Client.
In my React component I query with useQuery hook NODES_TYPE_ONE or NODES_TYPE_TWO based on a value blockData.myType. This works fine.
The GraphQL queries looks like:
export const NODES_TYPE_ONE = gql`
query GetNodesOne($customKey: String!) {
getNodesTypeOne(customKey: $customKey) {
nodes {
id
title
}
}
}
`;
export const NODES_TYPE_TWO = gql`
query GetNodesTwo($customKey: String!) {
getNodesTypeTwo(customKey: $customKey) {
nodes {
id
title
}
}
}
`;
But how do I type my data in GqlRes type?
When I console.log(data); I get: two different objects:
getNodesTypeOne {
nodes[// array of objects]
}
and
getNodesTypeTwo {
nodes[// array of objects]
}
My GqlRes type:
export type GqlRes = {
getNodesTypeOne: {
nodes: NodeTeaser[];
};
};
/** #jsx jsx */
import { useQuery } from '#apollo/client';
import { jsx } from '#emotion/react';
import { Slides } from 'app/components';
import { NODES_TYPE_ONE, NODES_TYPE_TWO } from './MyBlock.gql';
import { Props, GqlRes, NodesArgs } from './MyBlock.types';
const MyBlock = ({ data: blockData, metadata }: Props) => {
const customKey = metadata.customKey;
const { data } = useQuery<GqlRes, NodesArgs>(
blockData.myType === 'type-one' ? NODES_TYPE_ONE : NODES_TYPE_TWO,
{
variables: {
customKey: metadata.customKey || 0,
},
errorPolicy: 'all',
notifyOnNetworkStatusChange: true,
ssr: false,
}
);
const items =
data?.getNodesTypeOne.nodes.map((video) => {
return {
id: video.uuid,
type: 'type-one',
title: title,
};
}) || [];
return <Slides items={items} /> : null;
};
export default MyBlock;
Now my items returns only getNodesTypeOne but how do I get them both?
Update:
I created a union type for GqlRes:
type GetNodeTypeOne = {
getNodesTypeOne: {
nodes: Teaser[];
};
};
type GetNodeTypeTwo = {
getNodesTypeTwo: {
nodes: Teaser[];
};
};
export type GqlRes = GetNodeTypeOne | GetNodeTypeTwo;
But how do I map the nodes array now?
Update 2
As mention by #Urmzd I tried another approach. Just use multiple useQuery hooks:
const MyBlock = ({ data: blockData, metadata }: Props) => {
const customKey = metadata.customKey;
const { data: nodesOne } = useQuery<NodesOneGqlRes, NodesArgs>(NODES_TYPE_ONE,
{
variables: {
customKey: metadata.customKey || 0,
},
errorPolicy: 'all',
notifyOnNetworkStatusChange: true,
ssr: false,
}
);
const { data: nodesTwo } = useQuery<NodesTwoGqlRes, NodesArgs>(NODES_TYPE_TWO,
{
variables: {
customKey: metadata.customKey || 0,
},
errorPolicy: 'all',
notifyOnNetworkStatusChange: true,
ssr: false,
}
);
const items =
data?.// How do I get my nodes in a single variable?? .map((video) => {
return {
id: video.uuid,
type: 'type-one',
title: title,
};
}) || [];
return <Slides items={items} /> : null;
};
export default MyBlock;
But how do I map my data now, since I have two different GraphQL responses? And what is the best approach in this case?
If I understand your code directly then depending on the value of blockData.myType you're either executing one query or the other and you want to reuse the same useQuery hook for this logic. If you want that you'd need to make sure that GqlRes is a union type of getNodesTypeOne and getNodesTypeTwo.
// I don't know what NodeType is so I'm just using a string for this example
type NodeType = string
interface GetNodesTypeOne {
readonly getNodesTypeOne: {
readonly nodes: NodeType[]
}
}
interface GetNodesTypeTwo {
readonly getNodesTypeTwo: {
readonly nodes: NodeType[]
}
}
type GqlRes = GetNodesTypeOne | GetNodesTypeTwo
const resultOne:GqlRes = {
getNodesTypeOne: {
nodes: [ "test" ]
}
}
const resultTwo:GqlRes = {
getNodesTypeTwo: {
nodes: [ "test" ]
}
}
So this will solve the TypeScript issue. Then later in your code you're doing this:
const items = data?.getNodesTypeOne.nodes.map(...)
Since data may contain either getNodesTypeOne or getNodesTypeTwo we need to change this to something else. A quick fix would be to just select the first one that has values:
const nodes = "getNodesTypeOne" in data
? data?.getNodesTypeOne?.nodes
: data?.getNodesTypeTwo?.nodes
const items = nodes.map(...);
Or if you want to use the same condition:
const nodes = blockData.myType === 'type-one'
? (data as GetNodesTypeOne)?.getNodesTypeOne?.nodes
: (data as GetNodesTypeTwo)?.getNodesTypeTwo?.nodes
const items = nodes.map(...);
Note that in the second example we need to help TypeScript figure out the specific type by narrowing it down using a type assertion. In the first example this is not necessary because TypeScript is smart enough to figure out that the first expression will always result in a GetNodesTypeOne and the second expression will always result in a GetNodesTypeOne.
To answer your second question using the two separate queries:
Add a new variable useQueryOne which is true in case we're running query one and false in case we're running query two.
Add skip to useQuery to run only the appropriate query.
Add a new variable nodes that contains either the results from the first or from the second query (based on the useQueryOne condition)
const useQueryOne = blockData.myType === 'type-one';
const { data: nodesOne } = useQuery<NodesOneGqlRes, NodesArgs>(NODES_TYPE_ONE,
{
variables: {
customKey: metadata.customKey || 0,
},
errorPolicy: 'all',
notifyOnNetworkStatusChange: true,
ssr: false,
skip: !useQueryOne
}
);
const { data: nodesTwo } = useQuery<NodesTwoGqlRes, NodesArgs>(NODES_TYPE_TWO,
{
variables: {
customKey: metadata.customKey || 0,
},
errorPolicy: 'all',
notifyOnNetworkStatusChange: true,
ssr: false,
skip: useQueryOne
}
);
const nodes = useQueryOne
? nodesOne?.getNodesTypeOne?.nodes
: nodesTwo?.getNodesTypeTwo?.nodes;
const items = (nodes || []).map(...);
I should note that your queries are duplicates and hence should be refactored into a single query (unless they're just duplicate namespaces and not values).
Regardless, you can achieve your desired result and in a safer way using useLazyQuery
const [invokeQuery1, {loading, data, error}] = useLazyQuery<>(...)
const [invokeQuery2, {loading2, data2, error2}] = useLazyQuery<>(...)
// Run the query every time the condition changes.
useEffect(() => {
if (condition) {
invokeQuery1()
} else {
invokeQuery2()
}
}, [condition])
// Return the desired conditional daa
const {nodes} = useMemo(() => {
return condition ? data?.getNodesTypeOne : data2?.getNodesTypeTwo
} ,
[condition])
This also ensures that computation is not done needlessly (and you can invoke your queries based on events, as they should be).
-- Edit
Since you're adamant about using the union types (and mapping the data from the source).
Here's one possible, type-safe approach.
const isNodeTypeOne = (t: unknown): t is GetNodeTypeOne => {
return (t as GetNodeTypeOne).getNodesTypeOne !== undefined;
};
const { nodes } = isNodeTypeOne(data)
? data?.getNodesTypeOne
: data?.getNodesTypeTwo;
const items = nodes.map((val) => {
// Your mapping here
})
If you had different node types, you could also use predicates within the map.
I cannot figure out why prototypes in my function component in React fails with this:
`index.js:1 Warning: Failed prop type: The prop `profiles` is marked as required in `Home`, but its value is `undefined`.`
The app is working fine and Profiles is defined and I'm using React-redux with hooks and maybe that causing the issue becasue I don't know actually what to do to make the PropTypes to work
My home where this come ups:
import React, { useEffect, useState } from "react";
import { Row, Col, Jumbotron, Container, Image } from "react-bootstrap";
import { ProfileMiddleware } from "../Store/Middleware";
import { PropTypes } from "prop-types";
import { useDispatch, useSelector } from "react-redux";
import { USERNAME } from "../Services/constAPI";
import Experiences from "../Components/Experiences/Experiences";
import { Spinner } from "../Components/Spinner/Spinner.js";
const Home = () => {
const dispatch = useDispatch();
const { profiles, displaySpinner } = useSelector(state => ({
profiles: state.ProfileReducer.profiles,
displaySpinner: state.ProfileReducer.displaySpinner
}));
useEffect(() => {
dispatch(ProfileMiddleware.getOneProfile(USERNAME));
}, [dispatch]);
return !profiles.object ? (
<>
<Jumbotron>
<Container>
<Row>
<Col md={6}>
<Image src={profiles.imageUrl} alt="profile" roundedCircle />
</Col>
<Col md={6}>
<h1>{profiles.firstname + " " + profiles.surname}</h1>
<h4>{profiles.title}</h4>
<h5>{profiles.area}</h5>
<p>{profiles.email}</p>
<p>{profiles.bio}</p>
</Col>
</Row>
<Spinner displaySpinner={displaySpinner} />
</Container>
</Jumbotron>
<Experiences />
</>
) : (
<h3 className="red-text mt-5">The profile is not available</h3>
);
};
Home.propTypes = {
profiles: PropTypes.object.isRequired
};
export default Home;
The reducer as I'm using Redux
import { ProfileActions } from "../Actions";
function ProfileReducer(
state = {
profiles: {},
displaySpinner: false
},
action
) {
console.log("data in action", action.data);
console.log("Action type", action.type);
switch (action.type) {
case ProfileActions.GET_ONE_PROFILE:
return {
...state,
displaySpinner: true
};
case ProfileActions.GET_ONE_PROFILE_SUCCESS:
return {
...state,
profiles: action.data,
displaySpinner: false
};
default:
return state;
}
}
export default ProfileReducer;
I can show else if necessary but the APP works but PropTypes saying profiles are undefined that I cannot understand.
You're not passing in any props to Home. If you were, it would look something like
const Home = (props) => {
Instead, you are getting profiles from your redux store. So simply change
Home.propTypes = {
profiles: PropTypes.object.isRequired
};
to
Home.propTypes = {};
profiles is not a prop being passed to Home. The proptypes for the Home component should be deleted.
You say in the comments:
I have to check the profiles to be an obj with PropTypes I'm trying to find a solution to make it works. If you know a solution please share an answer
If you can guarantee that in your selector that's even better, but there're ways to do so in the component.
So let's take a snippet of your code and do that:
const Home = () => {
const dispatch = useDispatch();
const { profiles, displaySpinner } = useSelector(state => ({
profiles: state.ProfileReducer.profiles,
displaySpinner: state.ProfileReducer.displaySpinner
}));
// note Array's and other variable are also objects
// so we need to do a special check
const isProfilesAnObject = profiles && profiles.constructor.name === 'Object';
// created outside of `every` loop, and ternary shortcut to empty array
const profilesKeys = isProfilesAnObject ? Object.keys(profiles) : [];
// check that profileKeys is a subset of REQUIRED_KEYS
// n.b. you need to define this somewhere
const isProfilesCorrect = REQUIRED_KEYS.every(requiredKey => profileKeys.includes(requiredKey))
useEffect(() => {
dispatch(ProfileMiddleware.getOneProfile(USERNAME));
}, [dispatch]);
return isProfilesCorrect ?
This should work like below:
const checkProfiles = profiles => {
const REQUIRED_KEYS = ['a', 'b', 'z', 'y']
const isProfilesAnObject = profiles && profiles.constructor.name === 'Object';
// created outside of `every` loop, and ternary shortcut to empty array
const profilesKeys = isProfilesAnObject ? Object.keys(profiles) : [];
// check that profileKeys is a subset of REQUIRED_KEYS
// n.b. you need to define this somewhere
const isProfilesCorrect = REQUIRED_KEYS.every(requiredKey => profilesKeys.includes(requiredKey))
return isProfilesCorrect;
}
const [profile1, profile2, profile3] = [{
'a': '10'
}, {
a: '10', b: '20', z: '260', y: '250'
},{
a: '10', b: '20', extra_key: 'I\'m being a little bit extra', z: '260', y: '250'
}]
// missing keys
console.log(`profile1 (with keys ${Object.keys(profile1)}) is: ${checkProfiles(profile1) ?'correct': 'incorrect'}`);
// just the right number of keys
console.log(`profile2 (with keys ${Object.keys(profile2)}) is: ${checkProfiles(profile2) ?'correct': 'incorrect'}`);
// doesn't check if there aren't extra keys
console.log(`profile3 (with keys ${Object.keys(profile3)}) is: ${checkProfiles(profile3) ?'correct': 'incorrect'}`);
There is also invariant which you can use for stricter checking. It throws an error if something is false, so you can put exact checking in there, as seen below.
(n.b. ignore the process and module objects I had to define, and note I'm using tiny-invariant for convenience )
const checkProfiles = profiles => {
const REQUIRED_KEYS = ['a', 'b', 'z', 'y']
const isProfilesAnObject = profiles && profiles.constructor.name === 'Object';
// created outside of `every` loop, and ternary shortcut to empty array
const profilesKeys = isProfilesAnObject ? Object.keys(profiles) : [];
// check that profileKeys is a subset of REQUIRED_KEYS
// n.b. you need to define this somewhere
const isProfilesCorrect = REQUIRED_KEYS.every(requiredKey => profilesKeys.includes(requiredKey))
// really need to make sure 'z' exists and is a string
invariant(typeof profiles.z === 'string', `Profiles is expected to have a key 'z' that is a string, but found ${JSON.stringify(profiles.z)}`)
return isProfilesCorrect;
}
console.log(checkProfiles({
a: 10,
z: 'valid'
}))
try {
checkProfiles({
a: 10,
z: 20
});
} catch (e) {
console.error(e);
}
<script>
var process = {
env: 'production'
}
var module = {
exports: {}
}
</script>
<script src="https://cdn.jsdelivr.net/npm/tiny-invariant#1.2.0/dist/tiny-invariant.cjs.min.js">
</script>
Either of those should work depending on how seriously you need to make sure your variables exist and are correct
Actually, you didn't pass props, pay attention to:
const Home = () => {
You should write const Home = props => { or destruct the props in the beginning of your function component, like below:
const Home = ({ profiles }) => {
And then use it inside your execution context of the function component. Also, you can put the default value for your props like below:
const Home = ({ profiles = 'something' }) => {
The 'something' is a sample, it can be everything, or write like below at the end of your function component declaration:
Home.defaultProps = {
profiles: 'something'
};
I hope it helps you, but surely you should read the ReactJS docs a little bit more.
I'm messing with ag-grid, react-apollo, and everything seems to be working fine. The goal here is to click a check box and have a mutation / network request occur modifying some data. The issue i'm having is that it redraws the entire row which can be really slow but im really just trying to update the cell itself so its quick and the user experience is better. One thought i had was to do a optimistic update and just update my cache / utilize my cache. What are some approach you guys have taken.
Both the columns and row data are grabbed via a apollo query.
Heres some code:
CheckboxRenderer
import React, { Component } from "react";
import Checkbox from "#material-ui/core/Checkbox";
import _ from "lodash";
class CheckboxItem extends Component {
constructor(props) {
super(props);
this.state = {
value: false
};
this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
}
componentDidMount() {
this.setDefaultState();
}
setDefaultState() {
const { data, colDef, api } = this.props;
const { externalData } = api;
if (externalData && externalData.length > 0) {
if (_.find(data.roles, _.matchesProperty("name", colDef.headerName))) {
this.setState({
value: true
});
}
}
}
updateGridAssociation(checked) {
const { data, colDef } = this.props;
// const { externalData, entitySpec, fieldSpec } = this.props.api;
// console.log(data);
// console.log(colDef);
if (checked) {
this.props.api.assign(data.id, colDef.id);
return;
}
this.props.api.unassign(data.id, colDef.id);
return;
}
handleCheckboxChange(event) {
const checked = !this.state.value;
this.updateGridAssociation(checked);
this.setState({ value: checked });
}
render() {
return (
<Checkbox
checked={this.state.value}
onChange={this.handleCheckboxChange}
/>
);
}
}
export default CheckboxItem;
Grid itself:
import React, { Component } from "react";
import { graphql, compose } from "react-apollo";
import gql from "graphql-tag";
import Grid from "#material-ui/core/Grid";
import _ from "lodash";
import { AgGridReact } from "ag-grid-react";
import { CheckboxItem } from "../Grid";
import "ag-grid/dist/styles/ag-grid.css";
import "ag-grid/dist/styles/ag-theme-material.css";
class UserRole extends Component {
constructor(props) {
super(props);
this.api = null;
}
generateColumns = roles => {
const columns = [];
const initialColumn = {
headerName: "User Email",
editable: false,
field: "email"
};
columns.push(initialColumn);
_.forEach(roles, role => {
const roleColumn = {
headerName: role.name,
editable: false,
cellRendererFramework: CheckboxItem,
id: role.id,
suppressMenu: true,
suppressSorting: true
};
columns.push(roleColumn);
});
if (this.api.setColumnDefs && roles) {
this.api.setColumnDefs(columns);
}
return columns;
};
onGridReady = params => {
this.api = params.api;
this.columnApi = params.columnApi;
this.api.assign = (userId, roleId) => {
this.props.assignRole({
variables: { userId, roleId },
refetchQueries: () => ["allUserRoles", "isAuthenticated"]
});
};
this.api.unassign = (userId, roleId) => {
this.props.unassignRole({
variables: { userId, roleId },
refetchQueries: () => ["allUserRoles", "isAuthenticated"]
});
};
params.api.sizeColumnsToFit();
};
onGridSizeChanged = params => {
const gridWidth = document.getElementById("grid-wrapper").offsetWidth;
const columnsToShow = [];
const columnsToHide = [];
let totalColsWidth = 0;
const allColumns = params.columnApi.getAllColumns();
for (let i = 0; i < allColumns.length; i++) {
const column = allColumns[i];
totalColsWidth += column.getMinWidth();
if (totalColsWidth > gridWidth) {
columnsToHide.push(column.colId);
} else {
columnsToShow.push(column.colId);
}
}
params.columnApi.setColumnsVisible(columnsToShow, true);
params.columnApi.setColumnsVisible(columnsToHide, false);
params.api.sizeColumnsToFit();
};
onCellValueChanged = params => {};
render() {
console.log(this.props);
const { users, roles } = this.props.userRoles;
if (this.api) {
this.api.setColumnDefs(this.generateColumns(roles));
this.api.sizeColumnsToFit();
this.api.externalData = roles;
this.api.setRowData(_.cloneDeep(users));
}
return (
<Grid
item
xs={12}
sm={12}
className="ag-theme-material"
style={{
height: "80vh",
width: "100vh"
}}
>
<AgGridReact
onGridReady={this.onGridReady}
onGridSizeChanged={this.onGridSizeChanged}
columnDefs={[]}
enableSorting
pagination
paginationAutoPageSize
enableFilter
enableCellChangeFlash
rowData={_.cloneDeep(users)}
deltaRowDataMode={true}
getRowNodeId={data => data.id}
onCellValueChanged={this.onCellValueChanged}
/>
</Grid>
);
}
}
const userRolesQuery = gql`
query allUserRoles {
users {
id
email
roles {
id
name
}
}
roles {
id
name
}
}
`;
const unassignRole = gql`
mutation($userId: String!, $roleId: String!) {
unassignUserRole(userId: $userId, roleId: $roleId) {
id
email
roles {
id
name
}
}
}
`;
const assignRole = gql`
mutation($userId: String!, $roleId: String!) {
assignUserRole(userId: $userId, roleId: $roleId) {
id
email
roles {
id
name
}
}
}
`;
export default compose(
graphql(userRolesQuery, {
name: "userRoles",
options: { fetchPolicy: "cache-and-network" }
}),
graphql(unassignRole, {
name: "unassignRole"
}),
graphql(assignRole, {
name: "assignRole"
})
)(UserRole);
I don't know ag-grid but ... in this case making requests results in entire grid (UserRole component) redraw.
This is normal when you pass actions (to childs) affecting entire parent state (new data arrived in props => redraw).
You can avoid this by shouldComponentUpdate() - f.e. redraw only if rows amount changes.
But there is another problem - you're making optimistic changes (change checkbox state) - what if mutation fails? You have to handle apollo error and force redraw of entire grid - change was local (cell). This can be done f.e. by setting flag (using setState) and additional condition in shouldComponentUpdate.
The best way for me to deal with this was to do a shouldComponentUpdate with network statuses in apollo, which took some digging around to see what was happening:
/**
* Important to understand that we use network statuses given to us by apollo to take over, if either are 4 (refetch) we hack around it by not updating
* IF the statuses are also equal it indicates some sort of refetching is trying to take place
* #param {obj} nextProps [Next props passed into react lifecycle]
* #return {[boolean]} [true if should update, else its false to not]
*/
shouldComponentUpdate = nextProps => {
const prevNetStatus = this.props.userRoles.networkStatus;
const netStatus = nextProps.userRoles.networkStatus;
const error = nextProps.userRoles.networkStatus === 8;
if (error) {
return true;
}
return (
prevNetStatus !== netStatus && prevNetStatus !== 4 && netStatus !== 4
);
};
It basically says if there is a error, just rerender to be accurate (and i think this ok assuming that errors wont happen much but you never know) then I check to see if any of the network statuses are not 4 (refetch) if they are I dont want a rerender, let me do what I want without react interfering at that level. (Like updating a child component).
prevNetStatus !== netStatus
This part of the code is just saying I want the initial load only to cause a UI update. I believe it works from loading -> success as a network status and then if you refetch from success -> refetch -> success or something of that nature.
Essentially I just looked in my props for the query and saw what I could work with.