Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use refs in react through react-redux , withRouter?

I'm trying to use ref in a component connected to react-redux

I've tried this solution.

connect(null, null, null, {forwardRef: true})(myCom)

<myCom ref={ref => this.myCom = ref} />

this works just fine according to react-redux docs, but now when i try using

withRouter at the same time i get an error:

Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

so the final export code i have tried which lead to the above error

export default connect(null, null, null, { forwardRef: true })(withRouter(withStyles(styles)(myCom)));

Note: withStyles doesn't cause any issues as i've tried removing only withRouter, the issue was resolved.

is there any solution to solve this issue ?

like image 920
sasha romanov Avatar asked Aug 01 '19 16:08

sasha romanov


People also ask

How do you use withRouter in React?

React Router has an higher-order component called withRouter with which we can pass in the React Router's history, location, and match objects to our React components as props. To use withRouter , you should wrap your App component inside withRouter() as a parameter.

Can we store Ref in Redux?

You can keep them in your app's redux store in any persistence like web storage.

What is mapState to props in Redux?

As the first argument passed in to connect , mapStateToProps is used for selecting the part of the data from the store that the connected component needs. It's frequently referred to as just mapState for short. It is called every time the store state changes.

Is React forwardRef a hoc?

forwardRef() which means we have to apply the HOC before React.


2 Answers

In order to pass a ref to a component wrapped by withRouter you need to call it wrappedComponentRef. I recommend having withRouter as the outermost wrapper, so your example would look like the following:

withRouter(connect(null, null, null, {forwardRef: true})(myCom));
<myCom wrappedComponentRef={ref => this.myCom = ref} />

The following example and description are adapted from a related answer of mine: Get ref from connected redux component withStyles

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 (via the wrappedComponentRef property) 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. I am also displaying the location object from withRouter to demonstrate that withRouter 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";
import { withRouter } from "react-router-dom";

const styles = {
  list: {
    border: "1px solid blue"
  }
};

const TodoList = React.forwardRef(
  ({ todos, theme, classes, location }, ref) => (
    <>
      <div>Location (from withRouter): {JSON.stringify(location)}</div>
      <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 withRouter(
  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 wrappedComponentRef={todoListRef} />
      <VisibilityFilters />
      <button onClick={incrementRenderIndex}>
        Trigger re-render of TodoApp
      </button>
      <div>Render Index: {renderIndex}</div>
    </div>
  );
}

Edit Todo App with Redux

like image 98
Ryan Cogswell Avatar answered Oct 17 '22 09:10

Ryan Cogswell


Use compose method and try something like this

const enhance = compose(
  withStyles(styles),
  withRouter,
  connect(null, null, null, { forwardRef: true })
)

and use it before exporting component

export default enhance(MyComponent)
like image 25
tolotra Avatar answered Oct 17 '22 09:10

tolotra