Unable to use "this" in makeStyle - reactjs

I would like to create a dynamic background image depending on my props. So for that I wanted to make a react style and give it the picture stored in my state but I can't use this.state.pictures in it and I don't know why.
class DisplayArtist extends Component {
state = {
name : this.props.Info.artists.items[0].name,
followers: this.props.Info.artists.items[0].followers.total,
genres: this.props.Info.artists.items[0].genres,
picture: this.props.Info.artists.items[0].images[0]
}
useStyles = makeStyles({
root: {
backgroundImage={this.state.pictures}
}
});

makeStyles is better used in a functional component, rather than a class component.
using makeStyes inside a function component causes the style to be recreated on every render. I don't recommend doing it that way.
The recommended approach is to use inline styles for dynamic background images
e.g. style={{ backgroundImage: artist.images[0] }}
Converting to Functional Component
const DisplayArtist = (props) => {
const [ artist, setArtist ] = useState(null);
useEffect(() => {
//do your own checks on the props here.
const { name, total, genres, images } = props.Info.artists.items[0]
setArtist({name, total, genres, images});
},[props])
return ( <div style={{ width: '200px', height:'200px', backgroundImage: artist.images[0] }} /> )
}
export default DisplayArtist

You can use Functional Component and use useStyles and pass the state you want to it
const useStyles = makeStyles(() => ({
root: {
backgroundImage:({ picture }) => ( picture )
}
}))
function DisplayArtist({Info}) {
const [picture, setPicture] = useState()
const classes = useStyles({picture})
}

Use Higher-order component API Higher-Order-Component-Api
const styles = theme => ({
root: {
backgroundImage={this.state.pictures}
}
});
class DisplayArtist extends Component {
state = {
name : this.props.Info.artists.items[0].name,
followers: this.props.Info.artists.items[0].followers.total,
genres: this.props.Info.artists.items[0].genres,
picture: this.props.Info.artists.items[0].images[0]
}
}
export default withStyles(styles)(DisplayArtist);

Related

Observe (get sized) control (listen to events) over a nested component in the react and typescript application via the forwardRef function

I have a functional component called MyDivBlock
const MyDivBlock: FC<BoxProps> = ({ }) => {
{getting data...}
return (
<>
<div className='divBlock'>
{data.map((todo: { id: string; title: string }) =>
<div key={todo.id}>{todo.id} {todo.title} </div>)}
</div>
</>
);
};
I use it in such a way that MyDivBlock is nested as a child of
const App: NextPage = () => {
return (
<div>
<Box >
<MyDivBlock key="key0" areaText="DIV1" another="another"/>
</Box>
</div>
)
}
Note that MyDivBlock is nested in Box and MyDivBlock has no ref attribute. This is important because I need to write Box code with no additional requirements for my nested children. And anyone who will use my Box should not think about constraints and ref attributes.
Then I need to get the dimensions of MyDivBlock in the code of Box component, and later attach some event listeners to it, such as scrolling. These dimensions and listeners will be used in the Box component. I wanted to use Ref to control it. That is, the Box will later observe changes in the dimensions and events of MyDivBlock by creating a ref-reference to them
I know that this kind of parent-child relationship architecture is implemented through forwardRef
And here is the Box code:
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
export interface BoxProps extends React.ComponentProps<any> {
children?: Element[];
className: string;
}
export const Box: React.FC<BoxProps> = ({ children, ...rest }: BoxProps): JSX.Element => {
const childRef = useRef<HTMLDivElement>();
const ChildWithForwardRef = forwardRef<HTMLDivElement>((props, _ref) => {
const methods = {
show() {
if (childRef.current) {
console.log("childRef.current is present...");
React.Children.forEach(children, function (item) {
console.log(item)})
console.log("offsetWidth = " + childRef.current.offsetWidth);
} else {
console.log("childRef.current is UNDEFINED");
}
},
};
useImperativeHandle(_ref, () => (methods));
return <div ref={childRef}> {children} </div>
});
ChildWithForwardRef.displayName = 'ChildWithForwardRef';
return (
<div
className={'BoxArea'}>
<button name="ChildComp" onClick={() => childRef.current.show()}>get Width</button>
<ChildWithForwardRef ref={childRef} />
</div>
);
}
export default Box;
The result of pressing the button:
childRef.current is present...
[...]
$$typeof: Symbol(react.element) key: "key0" props: {areaText: 'DIV1', another: 'another'}
[...] Object
offsetWidth = undefined
As you can see from the output, the component is visible through the created ref. I can even make several nested ones and get the same for all of them.
But the problem is that I don't have access to the offsetWidth and other properties.
The other challenge is how can I add the addEventListener?
Because it works in pure Javascript with their objects like Element, Document, Window or any other object that supports events, and I have ReactChildren objects.
Plus I'm using NextJS and TypeScript.
Didn't dive too deep into the problem, but this may be because you are passing the same childRef to both div inside ChildWithForwardRef and to ChildWithForwardRef itself. The latter overwrites the former, so you have the method .show from useImperativeHandle available but not offsetWidth. A quick fix is to rewrite ChildWithForwardRef to use its own ref:
const ChildWithForwardRef = forwardRef<HTMLDivElement>((props, _ref) => {
const ref = useRef<HTMLDivElement>()
const methods = {
show() {
if (ref.current) {
console.log("ref.current is present...");
React.Children.forEach(children, (item) => console.log(item))
console.log("offsetWidth = " + ref.current.offsetWidth);
} else {
console.log("ref.current is UNDEFINED");
}
},
};
useImperativeHandle(_ref, () => (methods));
// Here ref instead of childRef
return <div ref={ref}> {children} </div>
});
But really I don't quite get why you would need ChildWithForwardRef at all. The code is basically equivalent to this simpler version:
const Box: React.FC<BoxProps> = ({ children, ...rest }: BoxProps): JSX.Element => {
const childRef = useRef<HTMLDivElement>();
const showWidth = () => {
if(childRef.current) {
console.log("childRef.current is present...");
React.Children.forEach(children, item => console.log(item))
console.log("offsetWidth = " + childRef.current.offsetWidth);
} else {
console.log("childRef.current is UNDEFINED");
}
}
return (
<div className={'BoxArea'}>
<button name="ChildComp" onClick={showWidth}>get Width</button>
<div ref={childRef}>{children}</div>
</div>
);
}
You can't solve this completely with React. I solved it by wrapping the child component, making it take the form of the parent.

How can I use a variable value for class name when using makeStyles?

In my React app, I have a component which takes in some data. Depending on the value of that data, I want to show the component with a different coloured background.
The styles are defined as:
const useStyles = makeStyles((theme) => ({
class1: {
backgroundColor: "red",
},
class2: {
backgroundColor: "pink",
},
}));
The component is:
const MyBox = ({ data }) => {
let classes = useStyles();
let innerClassName;
if (data.value) {
innerClassName = "class1";
} else {
innerClassName = "class2";
}
return (
<div className={innerClassName}>
Content goes here
</div>
);
};
export default MyBox;
However, this gives the component a class of "class1" or "class2", which doesn't get picked up by makeStyles. I also tried <div className={classes.innerClassName}> but then it looks for a class called 'innerClassName' which obviously it can't find.
I think I need to use some kind of variable string within <div className={????}> but I've tried various template literal strings and none of them have worked. What should I be doing?

nextjs react recoil persist values in local storage: initial page load in wrong state

I have the following code,
const Layout: React.FC<LayoutProps> = ({ children }) => {
const darkMode = useRecoilValue(darkModeAtom)
console.log('darkMode: ', darkMode)
return (
<div className={`max-w-6xl mx-auto my-2 ${darkMode ? 'dark' : ''}`}>
<Nav />
{children}
<style jsx global>{`
body {
background-color: ${darkMode ? '#12232e' : '#eefbfb'};
}
`}</style>
</div>
)
}
I am using recoil with recoil-persist.
So, when the darkMode value is true, the className should include a dark class, right? but it doesn't. I don't know what's wrong here. But it just doesn't work when I refresh for the first time, after that it works fine. I also tried with darkMode === true condition and it still doesn't work. You see the styled jsx, that works fine. That changes with the darkMode value and when I refresh it persists the data. But when I inspect I don't see the dark class in the first div. Also, when I console.log the darkMode value, I see true, but the dark class is not included.
Here's the sandbox link
Maybe it's a silly mistake, But I wasted a lot of time on this. So what am I doing wrong here?
The problem is that during SSR (server side rendering) there is no localStorage/Storage object available. So the resulted html coming from the server always has darkMode set to false. That's why you can see in cosole mismatched markup errors on hydration step.
I'd assume using some state that will always be false on the initial render (during hydration step) to match SSR'ed html but later will use actual darkMode value. Something like:
// themeStates.ts
import * as React from "react";
import { atom, useRecoilState } from "recoil";
import { recoilPersist } from "recoil-persist";
const { persistAtom } = recoilPersist();
export const darkModeAtom = atom<boolean>({
key: "darkMode",
default: false,
effects_UNSTABLE: [persistAtom]
});
export function useDarkMode() {
const [isInitial, setIsInitial] = React.useState(true);
const [darkModeStored, setDarkModeStored] = useRecoilState(darkModeAtom);
React.useEffect(() => {
setIsInitial(false);
}, []);
return [
isInitial === true ? false : darkModeStored,
setDarkModeStored
] as const;
}
And inside components use it like that:
// Layout.tsx
const [darkMode] = useDarkMode();
// Nav.tsx
const [darkMode, setDarkMode] = useDarkMode();
codesandbox link
Extending on #aleksxor solution, you can perform the useEffect once as follows.
First create an atom to handle the SSR completed state and a convenience function to set it.
import { atom, useSetRecoilState } from "recoil"
const ssrCompletedState = atom({
key: "SsrCompleted",
default: false,
})
export const useSsrComplectedState = () => {
const setSsrCompleted = useSetRecoilState(ssrCompletedState)
return () => setSsrCompleted(true)
}
Then in your code add the hook. Make sure it's an inner component to the Recoil provider.
const setSsrCompleted = useSsrComplectedState()
useEffect(setSsrCompleted, [setSsrCompleted])
Now create an atom effect to replace the recoil-persist persistAtom.
import { AtomEffect } from "recoil"
import { recoilPersist } from "recoil-persist"
const { persistAtom } = recoilPersist()
export const persistAtomEffect = <T>(param: Parameters<AtomEffect<T>>[0]) => {
param.getPromise(ssrCompletedState).then(() => persistAtom(param))
}
Now use this new function in your atom.
export const darkModeAtom = atom({
key: "darkMode",
default: false,
effects_UNSTABLE: [persistAtomEffect]
})

react functional component with ag grid cannot call parent function via context

I am using ag-grid-react and ag-grid-community version 22.1.1. My app is written using functional components and hooks. I have a cellrenderer component that is attempting to call a handler within the parent component using the example found here. Is this a bug in ag-grid? I have been working on this application for over a year as I learn React, and this is my last major blocker so any help or a place to go to get that help would be greatly appreciated.
Cell Renderer Component
import React from 'react';
import Button from '../../button/button';
const RowButtonRenderer = props => {
const editClickHandler = (props) => {
let d = props.data;
console.log(d);
props.context.foo({d});
//props.editClicked(props);
}
const deleteClickHandler = (props) => {
props.deleteClicked(props);
}
return (<span>
<Button classname={'rowbuttons'} onClick={() => { editClickHandler(props) }} caption={'Edit'} />
<Button classname={'rowbuttons'} onClick={() => { deleteClickHandler(props) }} caption={'Delete'} />
</span>);
};
export default RowButtonRenderer;
Parent Component
function Checking() {
function foo(props) {
let toggle = displayModal
setNewData(props);
setModalDisplay(!toggle);
}
const context = {componentParent: (props) => foo(props)};
const gridOptions = (params) => {
if (params.node.rowIndex % 2 === 0) {
return { background: "#ACC0C6" };
}
};
const frameworkComponents = {
rowButtonRenderer: RowButtonRenderer,
};
.
.
.
return (
<>
<AgGridReact
getRowStyle={gridOptions}
frameworkComponents={frameworkComponents}
context = {context}
columnDefs={columnDefinitions}
rowData={rowData}
headerHeight="50"
rowClass="gridFont"
></AgGridReact>
</>
);
}
When clicking the edit button on a row, the debugger says that there is a function.
This error is received though:
You are passing the context object in this code section:
const context = {componentParent: (props) => foo(props)};
...
<AgGridReact
context={context}
{...}
></AgGridReact>
And in your cell renderer you call this
props.context.foo({d});
While it should be this
props.context.componentParent({d});
Also you can assign your callback directly since it receives the same parameter and returns the same result (if any)
function foo(props) {
let toggle = displayModal
setNewData(props);
setModalDisplay(!toggle);
}
const context = {componentParent: foo};
You can also use this shorthand syntax from ES6 when assigning object property
function componentParent(props) {
let toggle = displayModal
setNewData(props);
setModalDisplay(!toggle);
}
const context = {componentParent};
Live Demo

Trigger re-render of subcomponent (react-table) using hooks

I'm still new to React, and functional programming, and Javascript, and JSX, so go easy if this is a stupid question.
I'm modifying one of the example material-ui tables from react-table v7. The original code can be found here. The example is completely functional and is using React Hooks as opposed to classes, as do all of the components of the template I'm using (shout out to creative-tim.com!)
My parent function (representative of a page in my dashboard application), for instance Users.js or Stations.js fetches data from a backend api inside a useEffect hook. That data is then passed as a prop to my subcomponent ReactTables.js
For some reason ReactTables.js does not receive changes to the "data" prop after the parent page's useEffect finishes. However, once I modify the data from a subcomponent of ReactTables (in this case AddAlarmDialog.js) then the table re-renders and all of my data suddenly appears.
How can I trigger the re-render of my subcomponent when data is returned from the parent component's useEffect? I noticed that in older versions of React there was a lifecycle function called componentWillReceiveProps(). Is this the behavior I need to emulate here?
Example Parent Component (Alarms.js):
import React, { useEffect, useState } from "react";
// #material-ui/core components
// components and whatnot
import GridContainer from "components/Grid/GridContainer.js";
import GridItem from "components/Grid/GridItem.js";
import ReactTables from "../../components/Table/ReactTables";
import { server } from "../../variables/sitevars.js";
export default function Alarms() {
const [columns] = useState([
{
Header: "Alarm Name",
accessor: "aName"
},
{
Header: "Location",
accessor: "aLocation"
},
{
Header: "Time",
accessor: "aTime"
},
{
Header: "Acknowledged",
accessor: "aAcked"
},
{
Header: "Active",
accessor: "aActive"
}
]);
const [data, setData] = useState([]);
const [tableType] = useState("");
const [tableLabel] = useState("Alarms");
useEffect(() => {
async function fetchData() {
const url = `${server}/admin/alarms/data`;
const response = await fetch(url);
var parsedJSON = JSON.parse(await response.json());
var tableElement = [];
parsedJSON.events.forEach(function(alarm) {
tableElement = [];
parsedJSON.tags.forEach(function(tag) {
if (alarm.TagID === tag.IDX) {
tableElement.aName = tag.Name;
}
});
tableElement.aTime = alarm.AlarmRcvdTime;
parsedJSON.sites.forEach(function(site) {
if (site.IDX === alarm.SiteID) {
tableElement.aLocation = site.Name;
}
});
if (alarm.Active) {
tableElement.aActive = true;
} else {
tableElement.aActive = false;
}
if (!alarm.AckedBy && !alarm.AckedTime) {
tableElement.aAcked = false;
} else {
tableElement.aAcked = true;
}
//const newData = data.concat([tableElement]);
//setData(newData);
data.push(tableElement);
});
}
fetchData().then(function() {
setData(data);
});
}, [data]);
return (
<div>
<GridContainer>
<GridItem xs={12} sm={12} md={12} lg={12}>
<ReactTables
data={data}
columns={columns}
tableType={tableType}
tableLabel={tableLabel}
></ReactTables>
</GridItem>
</GridContainer>
</div>
);
}
Universal Table Subcomponent (ReactTables.js):
import React, { useState } from "react";
// #material-ui/core components
import { makeStyles } from "#material-ui/core/styles";
// #material-ui/icons
import Assignment from "#material-ui/icons/Assignment";
// core components
import GridContainer from "components/Grid/GridContainer.js";
import GridItem from "components/Grid/GridItem.js";
import Card from "components/Card/Card.js";
import CardBody from "components/Card/CardBody.js";
import CardIcon from "components/Card/CardIcon.js";
import CardHeader from "components/Card/CardHeader.js";
import { cardTitle } from "assets/jss/material-dashboard-pro-react.js";
import PropTypes from "prop-types";
import EnhancedTable from "./subcomponents/EnhancedTable";
const styles = {
cardIconTitle: {
...cardTitle,
marginTop: "15px",
marginBottom: "0px"
}
};
const useStyles = makeStyles(styles);
export default function ReactTables(props) {
const [data, setData] = useState(props.data);
const [columns] = useState(props.columns);
const [tableType] = useState(props.tableType);
const [skipPageReset, setSkipPageReset] = useState(false)
const updateMyData = (rowIndex, columnId, value) => {
// We also turn on the flag to not reset the page
setData(old =>
old.map((row, index) => {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnId]: value
};
}
return row;
})
);
};
const classes = useStyles();
return (
<GridContainer>
<GridItem xs={12}>
<Card>
<CardHeader color="primary" icon>
<CardIcon color="primary">
<Assignment />
</CardIcon>
<h4 className={classes.cardIconTitle}>{props.tableLabel}</h4>
</CardHeader>
<CardBody>
<EnhancedTable
data={data}
columns={columns}
tableType={tableType}
setData={setData}
updateMyData={updateMyData}
skipPageReset={skipPageReset}
filterable
defaultPageSize={10}
showPaginationTop
useGlobalFilter
showPaginationBottom={false}
className="-striped -highlight"
/>
</CardBody>
</Card>
</GridItem>
</GridContainer>
);
}
ReactTables.propTypes = {
columns: PropTypes.array.isRequired,
data: PropTypes.array.isRequired,
tableType: PropTypes.string.isRequired,
tableLabel: PropTypes.string.isRequired,
updateMyData: PropTypes.func,
setData: PropTypes.func,
skipPageReset: PropTypes.bool
};
**For the record: if you notice superfluous code in the useEffect it's because I was messing around and trying to see if I could trigger a re-render.
I dont know exactly how the reactTable is handling its rendering, but if its a pure functional component, then the props you pass to it need to change before it will re-evaluate them. When checking if props have changed, react will just do a simple === comparison, which means that if your props are objects whos properties are being modified, then it will still evaluate as the same object. To solve this, you need to treat all props as immutable
In your example, you are pushing to the data array, and then calling setData(data) which means that you are passing the same instance of the array. When react compares the previous version of data, to the new version that you are setting in the call to setDate, it will think data hasnt changed because it is the same reference.
To solve this, you can just make a new array from the old array by spreading the existing array into a new one. So, instead of doing
data.push(tableElement);
You should do
const newInstance = [...data, tableElement];
Your code will need some tweaking because it looks like you are adding in lots of tableElements. But the short version of the lesson here is that you should never try and mutate your props. Always make a new instance
EDIT: So, after looking again, I think the problem is the way you are using the default param in the useState hook. It looks like you are expecting that to set the state from any prop changes, but in reality, that param is simply the default value that you will put in the component when it is first created. Changing the incoming data prop doesn't alter your state in any way.
If you want to update state in response to changes in props, you will need to use the useEffect hook, and set the prop in question as a dependancy.
But personally, I would try and not have what is essentially the same data duplicated in state in two places. I think the best bet would be to store your data in your alarm component, and add a dataChanged callback or something which will take your new data prop, and pass it back up to alarm via a parameter in the callback

Resources