Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Does v6 drop support for class components? #8146

Closed
AbrahamLopez10 opened this issue Oct 19, 2021 · 24 comments
Closed

Does v6 drop support for class components? #8146

AbrahamLopez10 opened this issue Oct 19, 2021 · 24 comments

Comments

@AbrahamLopez10
Copy link

First of all, thank you for making react-router available to the world :)

I'm not sure if this is the best place to ask about this but given that this is monitored by the maintainers I figured I could put my question here.

My question is around the fact that based on the currently available documentation for v6 (the migration/upgrade guide) react-router v6 seems to drop out-of-the-box support for class components and all the codebases that already have plenty of them (like ours) due to the following changes:

  1. Dropping support for the render prop in the Route component, which we are using with react-router v5 to pass params to the component (through props.match.params) of the route.

  2. Replacing the use of History (which was passed as a prop to our components automatically in v5) with the new NavigateFunction and the fact that it's only available through the useNavigate hook which of course is not accessible to class components.

Could someone shed the light on this and the future of react-router for all of us who are using class components and will most likely stick with using them for the foreseeable future?

Thanks in advance.

@afoures
Copy link

afoures commented Oct 19, 2021

I think that they are aware of this, and willing to do their best to make the transition as smooth as possible, check this tweet: https://twitter.com/mjackson/status/1448107013741989888?s=20

@timdorr
Copy link
Member

timdorr commented Oct 19, 2021

Hooks are the preferred API nowadays and are the main API surface in v6. The easiest way to get access to them is the same as any other hook, just wrap your class component with a functional component that passes along hook values as props:

const WrappedComponent = props => {
  const history = useHistory()
  const location = useLocation()

  return <Component history={history} location={location} {...props} />
}

You can also use more generic HOCs, like withRouter from v5, to pass along hooks to any component. This applies to any hook, not just React Router's.

@AbrahamLopez10
Copy link
Author

@timdorr Thanks. So will withRouter still be available in v6? My concern was around having some kind of support or adapter like this HOC for class components in v6, as introducing backwards incompatibility with class components or making it unnecessarily difficult to integrate with would be a serious regression for v6 when compared to v5.

BTW, "preferred" is relative to who you ask, though. It may be popular with several groups of developers but many other developers that come from backgrounds where classes are heavily used (e.g. .NET, Java, iOS development, etc.) are still working with and prefer class components. I see the value in using hooks in some scenarios but it's not a silver bullet and class components are a better approach in several cases.

@timdorr
Copy link
Member

timdorr commented Oct 19, 2021

I don't know if it will be available in v6. Likely it will be provided in some sort of support package to aid with the transition to the v6 APIs (react-router-compat, react-router-extras, or something like that).

@mjackson
Copy link
Member

mjackson commented Oct 20, 2021

We are absolutely not dropping support for class-based components, @AbrahamLopez10, but I think the API changes you pointed out do tend to illustrate some of the value of hooks.

For example, in a world without hooks it makes a lot of sense to use render props instead of a hook. This is because the router knows some state and it needs a way to expose it to you so it can pass it through to your component, so a render prop makes it easy to do so.

<Route
  path="posts/:id"
  render={({ match }) => <BlogPost id={match.params.id} />}
/>

class BlogPost extends React.Component {
  render() {
    let { id } = this.props;
    // ...
  }
}

However, in a world with hooks, we don't need a render prop for exposing state. Instead, you can just use the hook in your component directly.

<Route path="posts/:id" element={<BlogPost />} />

function BlogPost() {
  let { id } = useParams();
  // ...
}

In addition to eliminating the need for render props for sharing state, hooks also eliminate a lot of other prop-passing boilerplate code in other components that are rendered by <BlogPost>. For instance, without hooks if you wanted to pass the id URL param down to another component, you'd probably do something like this:

class BlogPost extends React.Component {
  render() {
    return (
      <div>
        <PostHeader id={this.props.id} />
        {/* ... */}
      </div>
    );
  }
}

class PostHeader extends React.Component {
  render() {
    let { id } = this.props;
    // ...
  }
}

But with hooks, it's as simple as:

function BlogPost() {
  return (
    <div>
      <PostHeader />
      {/* ... */}
    </div>
  );
}

function PostHeader() {
  let { id } = useParams();
  // ...
}

Anyway, that's my sales pitch for using hooks. I totally understand if you don't want to use them, but I think you could probably eliminate some of the boilerplate from your code if you did.

As for shipping a withRouter-style HOC with v6, we can probably do that if you really need one. But we would probably spend a bunch of time bike-shedding all the stuff you want to include in your HOC props (do you want the history? the match? just the params?) when in reality it's really easy to just make your own HOC that is custom designed for the pieces you need.

For example, you could make your own withNavigation or withParams HOC in a one-liner (again, thanks to the power of hooks!):

// in hocs.js
function withNavigation(Component) {
  return props => <Component {...props} navigate={useNavigate()} />;
}

function withParams(Component) {
  return props => <Component {...props} params={useParams()} />;
}

// in BlogPost.js
class BlogPost extends React.Component {
  render() {
    let { id } = this.props.params;
    // ...
  }
}

export default withParams(BlogPost);

Hopefully that helps you understand what we are thinking with v6. It's a low-level library and the hooks are a nice low-level abstraction that should allow you to build whatever you can think of and use whatever API you're most comfortable with, even if you decide to stick with class components.

@AbrahamLopez10
Copy link
Author

@mjackson Many thanks for the detailed clarification and the specific examples you provided, they were really helpful to clear my doubts.

I think if this info could be included in the v6 documentation to give peace of mind and direction to other developers who want or need to stick with class components it would be awesome so no one is left confused or discouraged by the hooks-centric approach in v6 😄

@paztis
Copy link

paztis commented Dec 7, 2021

It is easy to type it in javascript.
But less in typescript. Can you provide the correct signature for this ?

something like this ?

type TParamsProps = {
    params: Params<string>;
}

function withParams<P extends TParamsProps> (Component: ComponentType<P>): ComponentType<Omit<P, keyof TParamsProps>> {
    return props => <Component {...props} params={useParams()} />;
};

@yazdog8
Copy link

yazdog8 commented Jan 12, 2022

We are absolutely not dropping support for class-based components, @AbrahamLopez10, but I think the API changes you pointed out do tend to illustrate some of the value of hooks.

For example, in a world without hooks it makes a lot of sense to use render props instead of a hook. This is because the router knows some state and it needs a way to expose it to you so it can pass it through to your component, so a render prop makes it easy to do so.

<Route
  path="posts/:id"
  render={({ match }) => <BlogPost id={match.params.id} />}
/>

class BlogPost extends React.Component {
  render() {
    let { id } = this.props;
    // ...
  }
}

However, in a world with hooks, we don't need a render prop for exposing state. Instead, you can just use the hook in your component directly.

<Route path="posts/:id" element={<BlogPost />} />

function BlogPost() {
  let { id } = useParams();
  // ...
}

In addition to eliminating the need for render props for sharing state, hooks also eliminate a lot of other prop-passing boilerplate code in other components that are rendered by <BlogPost>. For instance, without hooks if you wanted to pass the id URL param down to another component, you'd probably do something like this:

class BlogPost extends React.Component {
  render() {
    return (
      <div>
        <PostHeader id={this.props.id} />
        {/* ... */}
      </div>
    );
  }
}

class PostHeader extends React.Component {
  render() {
    let { id } = this.props;
    // ...
  }
}

But with hooks, it's as simple as:

function BlogPost() {
  return (
    <div>
      <PostHeader />
      {/* ... */}
    </div>
  );
}

function PostHeader() {
  let { id } = useParams();
  // ...
}

Anyway, that's my sales pitch for using hooks. I totally understand if you don't want to use them, but I think you could probably eliminate some of the boilerplate from your code if you did.

As for shipping a withRouter-style HOC with v6, we can probably do that if you really need one. But we would probably spend a bunch of time bike-shedding all the stuff you want to include in your HOC props (do you want the history? the match? just the params?) when in reality it's really easy to just make your own HOC that is custom designed for the pieces you need.

For example, you could make your own withNavigation or withParams HOC in a one-liner (again, thanks to the power of hooks!):

// in hocs.js
function withNavigation(Component) {
  return props => <Component {...props} navigate={useNavigate()} />;
}

function withParams(Component) {
  return props => <Component {...props} params={useParams()} />;
}

// in BlogPost.js
class BlogPost extends React.Component {
  render() {
    let { id } = this.props.params;
    // ...
  }
}

export default withParams(BlogPost);

Hopefully that helps you understand what we are thinking with v6. It's a low-level library and the hooks are a nice low-level abstraction that should allow you to build whatever you can think of and use whatever API you're most comfortable with, even if you decide to stick with class components.

For some of us it's a matter of transitioning a code base that had used class to use hooks and it all can't be transitioned at once. We need the support for classes until we can remove them in favor of all hooks. This is common for a multitude of code bases.

@tumisangStarfire
Copy link

tumisangStarfire commented Jan 24, 2022

if you ever get stuck

import React,{ Component} from "react";
import { useNavigate } from "react-router-dom";

export const  withNavigation = (Component : Component) => {
  return props => <Component {...props} navigate={useNavigate()} />;
} 

//classComponent  
class LoginPage extends React.Component{

submitHandler =(e) =>{
    //successful login 
    this.props.navigate('/dashboard');
  }
}
 export default withNavigation(LoginPage);

@jefferson-liew
Copy link

if you ever get stuck

import React,{ Component} from "react";
import { useNavigate } from "react-router-dom";

export const  withNavigation = (Component : Component) => {
  return props => <Component {...props} navigate={useNavigate()} />;
} 

//classComponent  
class LoginPage extends React.Component{

submitHandler =(e) =>{
    //successful login 
    this.props.navigate('/dashboard');
  }
}
 export default withNavigation(LoginPage);

Is there a javascript alternative for this?

@kikonen
Copy link

kikonen commented Mar 27, 2022

if you ever get stuck

Ah thanks for this, took plenty of time on some strong words until finally finding this and getting react router to work. While it might sound.

@TechQuery
Copy link

TechQuery commented Apr 14, 2022

Official dropped, but we the people save ourselves:

@mguerrero-exe
Copy link

@paztis... this works for me

export const withRouter = (Component: React.ComponentType<any>) => {
  const WithRouter = (props: any) => {
    const location = useLocation();
    const navigate = useNavigate();
    const params = useParams();
    return <Component {...props} location={location} navigate={navigate} params={params} />;
  }
  return WithRouter;
}

@paztis
Copy link

paztis commented Apr 24, 2022

@paztis... this works for me

export const withRouter = (Component: React.ComponentType<any>) => {
  const WithRouter = (props: any) => {
    const location = useLocation();
    const navigate = useNavigate();
    const params = useParams();
    return <Component {...props} location={location} navigate={navigate} params={params} />;
  }
  return WithRouter;
}

Any is not a solution in typescript. WithRouter is supposed to support the generics, as before

@TechQuery
Copy link

Official dropped, but we the people save ourselves:

@MDGFUKAS You may use my package directly.

@paztis You may create a pull request to my package to enhance the generic of it.

@davidhicks980
Copy link

davidhicks980 commented Apr 26, 2022

Here is a typesafe solution that works with HOCs. Let me know if there are any issues.

import React, { ComponentType } from "react";
import {
  NavigateFunction,
  Params,
  useLocation,
  useNavigate,
  useParams,
  Location,
} from "react-router-dom";

interface RouterProps {
  navigate: NavigateFunction;
  readonly params: Params<string>;
  location: Location;
}

export type WithRouterProps<T> = T & RouterProps;
type OmitRouter<T> = Omit<T, keyof RouterProps>;

export function withRouter<T>(
  Component: ComponentType<OmitRouter<T> & RouterProps>
) {
  return (props: OmitRouter<T>) => {
    const location = useLocation();
    const navigate = useNavigate();
    const params = useParams();
    return (
      <Component
        location={location}
        navigate={navigate}
        params={params}
        {...props}
      />
    );
  };
}


/************* 
    Usage 
 *************/

interface TestProps {
  howdy: boolean;
  partner: boolean;
}

interface TestState {
  howdyPartner: string;
}

class TestComponent extends React.Component<
  WithRouterProps<TestProps>,
  TestState
> {
  state = { howdyPartner: "" };
  render() {
    return <h1>{this.props.location.pathname}</h1>;
  }
}

export default withRouter<TestProps>(TestComponent);

@RichardTMiles
Copy link

RichardTMiles commented May 27, 2022

import React from 'react';

import {
    BrowserRouter,
    Outlet,
    Route,
    Routes,
    useLocation,
    useNavigate,
    useParams
} from "react-router-dom";

import swal from '@sweetalert/with-react';

import {toast, ToastContainer} from 'react-toastify';

import 'react-toastify/dist/ReactToastify.min.css';

import {AxiosResponse} from "axios";

import WpAdmin from "src/components/WpAdmin/WpAdmin";

export function startInputChangeUpdateRoutine(
    shouldContinueAfterTimeout: () => boolean,
    cb: () => void,
    timeoutMs: number = 3000): () => void {

    const timer = () => setTimeout(() => {

        if (false === shouldContinueAfterTimeout()) {

            return;

        }

        cb();

    }, timeoutMs);

    const timerId = timer();

    return () => {
        clearTimeout(timerId);
    };

}

interface iBootstrapProperties extends iWebsocketState {
    children?: React.ReactNode | React.ReactNode[] | undefined;
}

interface iBootstrapState {
    id: string,
    axios: import("axios").AxiosInstance,
    authenticateURI: string,
    authenticated?: boolean,
    alert?: boolean
}

interface iBootStrapPublicFunctions {
    authenticate: () => void,
    changeLoggedInStatus: () => void,
    codeBlock: (markdown: String, highlight: String, language: String, dark: boolean) => any,
    passPropertiesAndRender: (Component: React.ElementType, props?: object) => any,
    semaphoreLock: Function,
    switchDarkAndLightTheme: () => void,
    testRestfulPostPutDeleteResponse: (response: AxiosResponse, success: Function | string | null, error: Function | string | null) => boolean,
}

export type iAmBootstrapDescendant = iBootstrapProperties & iBootstrapState & iBootStrapPublicFunctions;


function withRouter(Component: React.ComponentType<any>) {

    // noinspection UnnecessaryLocalVariableJS
    const WithRouter = (props: any) => {
        const location = useLocation();
        const navigate = useNavigate();
        const params = useParams();
        return <Component {...props} location={location} navigate={navigate} params={params} />;
    }

    return WithRouter;
}

class Bootstrap extends React.Component<iBootstrapProperties, iBootstrapState> implements iBootStrapPublicFunctions {

    constructor(props) {

        super(props);

        const contextAxios = context.axios;

        contextAxios.interceptors.request.use(
            req => {
                if (req.method === 'get' && req.url.match(/^\/rest\/.*$/)) {
                    req.params = JSON.stringify(req.params)
                }
                return req;
            },
            error => {
                return Promise.reject(error);
            }
        );

        contextAxios.interceptors.response.use(
            response => {
                // Do something with response data
                console.log(
                    "Every Axios response is logged in Bootstrap.tsx :: ",
                    response
                );
                if (response?.data?.alert) {
                    console.log("alert ∈ response");
                    this.handleResponseCodes(response);
                    return (response?.data?.alert?.error || response?.data?.alert?.danger) ?
                        Promise.reject(response) :
                        response;
                }
                return response;
            },
            async error => {

                /* Do something with response error
                   this changes from project to project depending on how your server uses response codes.
                   when you can control all errors universally from a single api, return Promise.reject(error);
                   is the way to go.
                */
                this.handleResponseCodes(error.response);

                console.log("Carbon Axios Caught A Response Error response :: ", error.response);

                return Promise.reject(error); // return error.response;

            });

        this.state = {
            id: '',
            axios: context.axios,
            authenticateURI: '/authenticated/',
            authenticated: null,
            alert: false
        };

    }
 
     passPropertiesAndRender = (Component: React.ComponentType<any>, props: object = {}) => {

        const RenderWithRouteProps = withRouter(Component)

        return <RenderWithRouteProps
            authenticate={this.authenticate}
            changeLoggedInStatus={this.changeLoggedInStatus}
            codeBlock={this.codeBlock}
            semaphoreLock={this.semaphoreLock}
            passPropertiesAndRender={this.passPropertiesAndRender}
            setCurrentUserObject={this.setCurrentUserObject}
            switchDarkAndLightTheme={this.switchDarkAndLightTheme}
            testRestfulPostPutDeleteResponse={this.testRestfulPostPutDeleteResponse}
            updateUser={this.updateUser}
            {...this.props}
            {...this.state}
            {...props} >
            <Outlet/>
        </RenderWithRouteProps>

    }

    testRestfulPostPutDeleteResponse = (response: AxiosResponse, success: Function | string | null, error: Function | string | null): boolean => {

        if (undefined !== response?.data?.rest
            && (('created' in response.data.rest)
                || ('updated' in response.data.rest)
                || ('deleted' in response.data.rest))
        ) {

            if (typeof success === 'function') {

                return success(response);

            }

            if (success === null || typeof success === 'string') {

                toast.success(success, this.state.toastOptions);

            }

            return response.data.rest?.created ?? response.data.rest?.updated ?? response.data.rest?.deleted ?? true;

        }

        if (typeof error === 'function') {

            return error(response);

        }

        if (error === null || typeof error === 'string') {

            toast.success(error, this.state.toastOptions);

        }

        return false;

    };


    render() {

        console.log("BOOTSTRAP TSX RENDER");

        const {alert} = this.state

        const ppr = this.passPropertiesAndRender;

        return <BrowserRouter>
            <Routes>
                <Route path='/*' element={ppr(DropInGaming, {})}/>
                <Route path='wp-admin/*' element={ppr(WpAdmin, {})}/>
            </Routes>
            {alert}
        </BrowserRouter>
    }

}

// @ts-ignore
export default Bootstrap;

^^ full call impl

@vijaya-lakshmi-venkatraman
Copy link

if you ever get stuck

import React,{ Component} from "react";
import { useNavigate } from "react-router-dom";

export const  withNavigation = (Component : Component) => {
  return props => <Component {...props} navigate={useNavigate()} />;
} 

//classComponent  
class LoginPage extends React.Component{

submitHandler =(e) =>{
    //successful login 
    this.props.navigate('/dashboard');
  }
}
 export default withNavigation(LoginPage);

Thank you for this.
To pass data along with navigate() in class A (wrap with useNavigate) and fetch it in class B (wrap with useLocation) :
In Class A:
this.props.navigate(path, {state: {someparam: 3,}})

In Class B:
this.props.location.state.someparam

@rajat153
Copy link

how to use route props in render like:
{/* <Route path = "/food/:name" render={(props) => <Food text="Hello, " {...props} />} /> */}
i dont know why mine isnot working

@Johnne25
Copy link

Johnne25 commented Aug 8, 2022

Although its said support for Class Components isn't being dropped, the fact that you have to wrap your class in a HOC in order to get it to work doesn't support that statement.

@h110m
Copy link

h110m commented Aug 19, 2022

@Johnne25
Its like saying you not dropping it but dropping it...

@MTraveller
Copy link

MTraveller commented Sep 4, 2022

const WrappedComponent = props => {
  const history = useHistory()
  const location = useLocation()

  return <Component history={history} location={location} {...props} />
}

Works great, even with dynamic components:

// Routes
import MovieForm from "./components/movieForm";
import ElementWrapper from "./components/elementWrapper";

<Route path="/movies/new" element={<ElementWrapper routeElement={MovieForm} />} />
// Wrapper 
import React from "react";
import { useLocation, useParams } from "react-router-dom";

const ElementWrapper = (props) => {
  const params = useParams();
  const locations = useLocation();
  const Element = props.routeElement;

  return <Element params={params} locations={locations} {...props} />;
};

export default ElementWrapper;

@Hamadasatco
Copy link

Hamadasatco commented Sep 11, 2022

// I have this code but i couldn't update it i wrapped the // shop Component with WithRoute it didn't work
// I don't know what Component should i wrap.
// Please help.

import React, { Component } from "react";
import { Switch, Route, Redirect } from "react-router-dom";
import { connect } from "react-redux";
import { loadData } from "../data/ActionCreators"; import { DataTypes } from "../data/Types";
import { Shop } from "./Shop";

const mapStateToProps = (dataStore) => ({
...dataStore })

const mapDispatchToProps = {
loadData
}

const filterProducts = (products = [], category) => (!category || category === "All") ? products : products.filter(p =>
p.category.toLowerCase() === category.toLowerCase());

export const ShopConnector = connect(mapStateToProps, mapDispatchToProps)( class extends Component {
render() {
return
<Route path="/shop/products/:category?"
render={ (routeProps) =>
<Shop { ...this.props } { ...routeProps } products={ filterProducts(this.props.products, routeProps.match.params.category) } />} />

}

componentDidMount() { this.props.loadData(DataTypes.CATEGORIES); this.props.loadData(DataTypes.PRODUCTS);
} } )

@NickCarducci
Copy link

To be clear:

You can't use Hooks inside a class component, but you can definitely mix classes and function components with Hooks in a single tree.

class App extends React.Component {render(){return <div/>}}
const ClassHook = () => {
  return <App paths={useParams()} l={useLocation()} n={useNavigate()} />;
}; // "cannot be called inside a callback" <Hook/>
createRoot(document.getElementById("root")).render(
  <BrowserRouter>
    <Routes>
      <Route
        //exact
        path="/*"
        //children,render
        element={<ClassHook />} //Initelement
      />
    </Routes>
  </BrowserRouter>
);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests