Get ref from connected redux component withStyles - reactjs

I have this export of a working component:
export default connect(
mapStateToProps, actions,
null, { withRef: true, forwardRef: true }
)(withTheme()(withStyles(styles)(MainMenu)));
And its call:
<MainMenu
ref={(connectedMenu) => this.menuRef = connectedMenu.getWrappedInstance()}
user={user}
/>
I've expected to get a MainMenu ref, but I keep getting WithTheme object instead.
I've also tried to get through innerRef, but got the following errors:
TypeError: connectedMenu.getWrappedInstance is not a function
TypeError: Cannot read property 'getWrappedInstance' of null
Before all of that I've tried that React.createRef() format, but it didn't worked.
How do I get this ref?

Assuming you are using v4 of Material-UI, your syntax for withTheme is incorrect. In v4 the first set of parentheses was removed.
Instead of
withTheme()(YourComponent)
you should have
withTheme(YourComponent)
Below is code from a modified version of the react-redux todo list tutorial that shows the correct syntax. I've included here the two files that I changed (TodoList.js and TodoApp.js), but the sandbox is a fully working example.
In TodoApp, I use the ref on TodoList to get and display its height. The displayed height will only get updated if TodoApp re-renders, so I've included a button to trigger a re-render. If you add a couple todos to the todo list, and then click the re-render button, you will see that the new height of the list is displayed (showing that the ref is fully working).
In TodoList, I'm using withStyles to add a blue border around the todo list to show that withStyles is working, and I'm displaying the primary color from the theme to show that withTheme is working.
TodoList.js
import React from "react";
import { connect } from "react-redux";
import Todo from "./Todo";
import { getTodosByVisibilityFilter } from "../redux/selectors";
import { withStyles, withTheme } from "#material-ui/core/styles";
import clsx from "clsx";
const styles = {
list: {
border: "1px solid blue"
}
};
const TodoList = React.forwardRef(({ todos, theme, classes }, ref) => (
<>
<div>theme.palette.primary.main: {theme.palette.primary.main}</div>
<ul ref={ref} className={clsx("todo-list", classes.list)}>
{todos && todos.length
? todos.map((todo, index) => {
return <Todo key={`todo-${todo.id}`} todo={todo} />;
})
: "No todos, yay!"}
</ul>
</>
));
const mapStateToProps = state => {
const { visibilityFilter } = state;
const todos = getTodosByVisibilityFilter(state, visibilityFilter);
return { todos };
};
export default connect(
mapStateToProps,
null,
null,
{ forwardRef: true }
)(withTheme(withStyles(styles)(TodoList)));
TodoApp.js
import React from "react";
import AddTodo from "./components/AddTodo";
import TodoList from "./components/TodoList";
import VisibilityFilters from "./components/VisibilityFilters";
import "./styles.css";
export default function TodoApp() {
const [renderIndex, incrementRenderIndex] = React.useReducer(
prevRenderIndex => prevRenderIndex + 1,
0
);
const todoListRef = React.useRef();
const heightDisplayRef = React.useRef();
React.useEffect(() => {
if (todoListRef.current && heightDisplayRef.current) {
heightDisplayRef.current.innerHTML = ` (height: ${
todoListRef.current.offsetHeight
})`;
}
});
return (
<div className="todo-app">
<h1>
Todo List
<span ref={heightDisplayRef} />
</h1>
<AddTodo />
<TodoList ref={todoListRef} />
<VisibilityFilters />
<button onClick={incrementRenderIndex}>
Trigger re-render of TodoApp
</button>
<div>Render Index: {renderIndex}</div>
</div>
);
}

Related

How to prevent component from being re-rendered unnecessarily

I'll start with the code. I have a stateless functional component that resembles this
export const Edit Topic = (_title, _text) {
const [title, setTitle] = useState(_title)
const [text, setText] = useState(_text)
return (
<>
<InputText props={{ fieldName:"Title:", value:title, setValue:setTitle, placeHolder:"Topic Title"}}/>
<InputTextArea props={{ fieldName:"Markdown Text:", text, setText }}/>
<PreviewBox text={text}/>
</>
)
}
I have PreviewBox when it's on, page rendering takes a bit longer because text can be quite long. PreviewBox needs to re-render each time I change text in InputTextArea and that's fine.
The problem I'm having is when I change the value of title it's also updating <PreviewBox/> which is undesired.
How can I make sure that <PreviewBox/> only updates when text changes and not when title changes?
The reason why I believe the re-rendering is occuring is because if I toggle off PreviewBox, there's no lag in when updating title but when PreviewBox is visible the updating the title lags.
import style from "../styles/CreateTopic.module.css"
import { Component } from "react"
import Markdown from "./Markdown";
export class PreviewBox extends Component {
constructor(props) {
super(props)
this.state = {
isShow: true
}
}
toggleShow = () => {
console.log("begin isShow", this.state)
this.setState(state => ({ isShow: !state.isShow}))
}
render() {
return (
<>
<div className={style.wrptoggle}>
<button className={style.btn} onClick={this.toggleShow}>Preview</button>
</div>
{this.state.isShow ?
<div className={style.wrppreviewbox}>
<div className={style.previewbox}>
<Markdown text={this.props.text}/>
</div>
</div>
: null}
</>
)
}
}
Since the above also contains <Markdown/> here's that component:
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import ReactMarkdown from "react-markdown";
import "katex/dist/katex.min.css";
const Markdown = ({text}) => {
return (
<div>
<ReactMarkdown
remarkPlugins={[remarkMath]}
rehypePlugins={[rehypeKatex]}
children={text}
/>
</div>
);
}
export default Markdown;
I don't see any complexity in PreviewBox that would cause any rendering delay so I might assume it's the Markdown component that may take some time "working" when it's rerendered since you say "toggle off PreviewBox, there's no lag in when updating title".
Solution
You can use the memo Higher Order Component to decorate the Markdown component and provide a custom areEqual props compare function.
import { memo } from 'react';
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import ReactMarkdown from "react-markdown";
import "katex/dist/katex.min.css";
const Markdown = ({ text }) => {
return (
<div>
<ReactMarkdown
remarkPlugins={[remarkMath]}
rehypePlugins={[rehypeKatex]}
children={text}
/>
</div>
);
};
export default memo(Markdown);
By default it will only shallowly compare complex objects in the props
object. If you want control over the comparison, you can also provide
a custom comparison function as the second argument.
const areEqual = (prevProps, nextProps) => {
return prevProps.text === nextProps.text;
};
export default memo(Markdown, areEqual);

Map through two arrays of components and strings and render in one component

I have two arrays that I want to map through:
const social = ["Snapchat", "TikTok", "Dribbble", "Discord", "Facebook"];
const socialIcons = [<SnapchatIcon />, <DribbbleIcon />];
The socialIcons array are all components
How can I send both values as props into my DummyRectangle component? Here is my current code:
{social.map((s, index) => (
<div className="dummy_buttonsWrapper">
<DummRectangle social={s} socialIcons={i} />
</div>
))}
And here is DummyRectangle component:
function DummRectangle({ social, socialIcons }) {
// console.log("---->", socialIcons);
return (
<div>
<p>{social}</p>
{<socialIcon/>} // render social icon component
</div>
);
}
To do so, you don't need to wrap tags around your socialIcon in your DummRectangle. Also, it doesn't seem that you are passing the socialIcon component at all. If I were you, I would do something like this:
The following two are the components as an example that you would like to render (in your case - socialIcons)
// Comp1.js
import React from "react";
const Comp1 = () => <div>actual Comp1</div>;
export default Comp1;
// Comp2.js
import React from "react";
const Comp2 = () => <div>actual Comp2</div>;
export default Comp2;
Now, in your main Parent component, you would simply get the current component of the componentName (in your case - social) by accessing your component's array with an index. Then, you would pass this currentComponent as props to your Child component where you want to render it.
// App.js
import React from "react";
import Comp1 from "./Comp1";
import Comp2 from "./Comp2";
import DummyComponent from "./DummyComponent";
export default function App() {
const componentNames = ["Comp1", "Comp2"];
const components = [<Comp1 />, <Comp2 />];
return (
<div className="App">
{componentNames.map((name, index) => {
const currentComponent = components[index];
return (
<div>
<DummyComponent componentName={name} component={currentComponent} />
</div>
);
})}
</div>
);
}
In your Child component, you can simply render it by enclosing it into the brackets - no need to add tags. React will do all the rendering for you. In your case it would be { socialIcon }
// DummyComponent.js
import React from "react";
const DummyComponent = ({ componentName, component }) => {
return (
<div>
<p>{componentName}</p>
{component}
</div>
);
};
export default DummyComponent;
Link to Codesandbox with the above code for reference: click here

How can I update state in two separate components using a custom hook?

I am trying to create a custom hook to be able to open and close a pop-out menu with conditional rendering using style display: none and display:block. I think I understand how to share the state between the components (I can console log that and get that working) , but I can not figure out how to update the state using the hook.
I am certain that I have some fundamental misunderstanding here but if anyone can clarify what it is I am trying to achieve that would be awesome! I have tried to learn this for several nights and here is where I have got to.
This is the header of the pop out menu it only contains a close button at the moment
import React from 'react'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faWindowClose } from '#fortawesome/free-solid-svg-icons'
import useOpenCloseElementMenu from '../Hooks/openCloseElementMenu'
function ElementMenuHeader() {
const { elementMenuOpenClose, setElementMenuOpenClose } = useOpenCloseElementMenu();
return (
<div id="App-Close-Element-Menu-Container">
<button id="App-Close-Element-Menu"
onClick={() => setElementMenuOpenClose(false) }
>
<FontAwesomeIcon icon={faWindowClose} />
</button>
</div>
);
}
export default ElementMenuHeader
This is the pop out menu
import React from 'react';
import SizerGroup from '../Sizer/sizerGroup';
import './element-menu.css';
import ElementMenuHeader from './element-menu-header';
import TitleWithLine from './title-with-line';
import TypeSelector from './type-selector';
import TemplateSelector from './template-selector';
import useOpenCloseElementMenu from '../Hooks/openCloseElementMenu'
function Editor(props) {
const { elementMenuOpenClose, setElementMenuOpenClose } = useOpenCloseElementMenu();
console.log(elementMenuOpenClose);
return (
<div className="App-Element-Menu"
style={{display: elementMenuOpenClose ? 'block' : 'none' }}
>
<ElementMenuHeader />
<TitleWithLine title="Element size" />
<SizerGroup />
<TitleWithLine title="Elements" />
<TypeSelector />
<TitleWithLine title="Templates" />
<TemplateSelector />
</div>
);
}
export default Editor
This is the toolbar that has the open menu button
import React from 'react'
import Button from '../Button/button'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faBoxes } from '#fortawesome/free-solid-svg-icons'
import './toolbar.css'
import useOpenCloseElementMenu from '../Hooks/openCloseElementMenu'
function Toolbar(props) {
const { toolbar_show_or_hide } = props
const elementMenuIcon = <FontAwesomeIcon icon={ faBoxes } />
const { elementMenuOpenClose, setElementMenuOpenClose } = useOpenCloseElementMenu();
const openEditor = setElementMenuOpenClose[true]
return (
<div className="App-Toolbar" style={{ display: toolbar_show_or_hide ? "flex" : "none" }} >
<Button
id="App-Open-Element-Menu-Button"
icon={ elementMenuIcon }
useToolTip={ true }
toolTipText="Elements menu. Select elements to populate the theme."
buttonFunction={ openEditor }
/>
</div>
)
}
export default Toolbar
This is the hook
import React, { useState } from 'react';
const useOpenCloseElementMenu = () => {
const [elementMenuOpenClose, setElementMenuOpenClose] = useState(false);
return { elementMenuOpenClose, setElementMenuOpenClose };
};
export default useOpenCloseElementMenu;
I feel you donot have to pass the hook in a separate function, useOpenCloseElementMenu, like you did.
Instead of importing the ../Hooks/openCloseElementMenu function thing,
I'd rather just call the hooks directly instead as
const [elementMenuOpenClose, setElementMenuOpenClose] = useState(false);
in the editor and toolbar component in place of const { elementMenuOpenClose, setElementMenuOpenClose } = useOpenCloseElementMenu();.
Also which component did you use the toolbar component if I may ask? Because I don't seem to see any of that here...It confusing where it got the toolbar_show... props from.
{I hope this helps cause that seem like the most obvious reason}

React REDUX not updating the state to the new one

Just trying to build a simple react-redux code here. Whenever I click one of the button with some data value attached to it, I just want it to use it to update state of my react app.
The directory composition is :
src/index.js
src/App.js
src/reduxButtonGroup.js
src/reduxActions.js
src/reducers/index.js
src/reduxStore.js
src/reduxHelloWorld.js
Now, coming to the codes here is how App.js looks like:
import React from 'react';
import HelloWorld from './reduxHelloWorld.js';
import {store} from './reduxStore.js'
import ButtonGroup from './reduxButtonGroup.js';
import 'bootstrap/dist/css/bootstrap.min.css';
class App extends React.Component {
render(){
return(
<React.Fragment>
<HelloWorld name = {store.getState().name} />
<ButtonGroup names = {["NameA","NameB","NameC"]}/>
</React.Fragment>
)
}
}
export default App;
Here is how reduxButtonGroup.js looks like:
import React from 'react';
import Button from 'react-bootstrap/Button';
import {store} from './reduxStore.js';
import {setName} from './reduxActions.js';
const ButtonGroup = ( {names} ) => (
<div>
{names.map((name,i) => (
<Button data-name={name} key={`btn-${i}`} style={{marginRight: "15px"}} className={"btn btn-danger"} onClick={dispatchBtnAction}>
{name}
</Button>
))}
</div>
);
function dispatchBtnAction(e){
const name = e.target.dataset.name;
console.log(name);
store.dispatch(setName(name));
}
export default ButtonGroup;
And reduxActions.js:
export function setName (name) {
return {
type: "SET_NAME",
name: name
}
}
Also, reducers/index.js:
export default (state,action) => {
console.log(action.name)
switch(action.type){
case "SET_NAME":
console.log("here")
return{
...state,
name: action.name
};
default:
return state;
}
};
Here is how reduxStore.js looks like:
import {createStore} from 'redux';
import reducer from "./reducers";
const initialState = { name: "initialName " };
export const store = createStore(reducer, initialState);
And at last reduxHelloWorld.js:
import React from 'react';
class HelloWorld extends React.Component{
render(){
return(
<React.Fragment>
<h1>Hello {this.props.name}</h1>
</React.Fragment>
)
}
}
export default HelloWorld;
Now, in the terminal I can see the right values in action.type and action.name . However I think that somehow the part shown below is not working. i.e., I'm unable to update my state variable called name to action.name .
return{
...state,
name: action.name
};
Though I'm getting the right values, my component is not getting rendered with the right values. Any idea what I'm doing wrong over here. Any help would be highly appreciated.

React exporting withRouter and withStyles error

I am using react along with redux and material-ui to make a component. I am attempting to write an export statement export default connect()(withRouter(FirstPage))(withStyles(styles)(FirstPage))
However, this doesn't seem to work I get an error that says
TypeError: Cannot set property 'props' of undefined
this.props = props;
This error is referencing one of my node_modules.
Here is my full code:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {withRouter} from 'react-router-dom'
import { withStyles } from '#material-ui/core/styles';
import Card from '#material-ui/core/Card';
import CardActions from '#material-ui/core/CardActions';
import CardContent from '#material-ui/core/CardContent';
import Button from '#material-ui/core/Button';
const styles = theme =>({
root: {
maxWidth: 345,
},
})
class FirstPage extends Component {
state = {
feeling: ''
}
//This function will dispatch the users response to index.js
//The dispatch type here is 'SET_FEELING'
submitData=(event) => {
event.preventDefault();
this.props.dispatch({type: 'SET_FEELING', payload: this.state})
this.changeLocation();
}
//This function will update the local state with the users response
handleChange= (event) => {
this.setState({
feeling: event.target.value
})
}
//This function will change the current url when a button is clicked
changeLocation= ()=> {
this.props.history.push('/secondPage')
}
render(){
const { classes } = this.props;
return(
<div>
<Card >
<CardContent className={classes.root}>
<form>
<input onChange={this.handleChange} placeholder='How are you feeling' value={this.state.feeling} />
</form>
</CardContent>
<CardActions>
<Button onClick={this.submitData}>Submit</Button>
</CardActions>
</Card>
</div>
)
}
}
//this export connects the component to the reduxStore as well as allowing us to use the history props
export default connect()(withRouter(FirstPage))(withStyles(styles)(FirstPage))
I believe the following code should work:
export default withRouter(connect()(withStyles(styles)(FirstPage)))
Instead of
export default connect()(withRouter(FirstPage))(withStyles(styles)(FirstPage))
First of all, connect() returns a function that only accepts an argument. Second, connect() should be wrapped inside withRouter(). This problem is stated in the github docs of React Router.
without using react-redux :
export default (withStyles(styles), withRouter)(FirstPage);

Resources