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 ?
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.
You can keep them in your app's redux store in any persistence like web storage.
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.
forwardRef() which means we have to apply the HOC before React.
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>
);
}
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With