Search code examples
reactjsreduxreact-reduxtsx

I am having teething problems with React + Redux for private page / public page authentication support


I've been reading about React for a while, and I understand some of the concepts, I'm also fairly familiar with SAP UI5, and Knockoutjs. I've started a new React + Redux template in VS2019, this example has weather forecast and counter examples.

Before I really start on my application, I wanted to sort out the authentication side of things - only needs to be a fake login. I have added an auth session store following the examples in the counter store, with authenticate and I have added login and logout controllers all of which work.

My Login Component thus far looks like this:

import * as React from "react";
import { connect } from "react-redux";
import { RouteComponentProps } from 'react-router';
import { ApplicationState } from '../store';
import * as AuthSession from '../store/AuthSession';

type AuthProps =
    AuthSession.AuthState &
    typeof AuthSession.actionCreators &
    RouteComponentProps<{}>;


class Login extends React.PureComponent<AuthProps> {
    public render() {
        return (
            <React.Fragment>
                <h1>Login</h1>

                <p aria-live="polite">Current IsAuthenticated= <strong>{String(this.props.IsAuthenticated)}</strong></p>
                <p aria-live="polite">Current uuid= <strong>{this.props.uuid}</strong></p>

                <button type="button"
                    className="btn btn-primary btn-lg"
                    onClick={() => { this.props.auth(); }}>
                    Auth
                </button>

            </React.Fragment>
        );
    }
};

export default connect(
    (state: ApplicationState) => state.auth,
    AuthSession.actionCreators
)(Login);

I have also been following a blog relating to creating LoggedInRoute and LoggedOutRoute components to bring in and out aspects of the site. The blog was using arrow functions instead of classes however and I couldn't get my head around that, so I set off on my own journey. I'm really having problems passing both my state from the redux store in addition to my Component Tag's attributes (props). My effort thus far looks like this:

import * as React from "react";
import { connect } from "react-redux";
import { Route } from "react-router-dom";
import { RouteComponentProps } from 'react-router';
import { ApplicationState } from '../store';
import * as AuthSession from '../store/AuthSession';

type ParamProps = {
    exact?: boolean;
    path: string;
    component: React.ComponentType<any>;
    otherProps: any
};

type ComponentProps =
    AuthSession.AuthState &
    typeof AuthSession.actionCreators &
    RouteComponentProps<{}> &
    ParamProps;


class LoggedInRoute extends React.PureComponent<ComponentProps> {
    public render() {
        if (this.props.IsAuthenticated === true) {
            return (
                <React.Fragment>
                    <p aria-live="polite">Current IsAuthenticated= <strong>{String(this.props.IsAuthenticated)}</strong></p>
                    <p aria-live="polite">Current uuid= <strong>{this.props.uuid}</strong></p>

                    <Route
                        render={otherProps => (
                            <>
                                <React.Component {...otherProps} />
                            </>
                        )}
                    />
                </React.Fragment>
            );
        } else {
            return null;
        }
    }
};

export default connect(
    (state: ApplicationState, ownProps: ParamProps) => {
        return { ...state.auth, ...ownProps }
    },
    AuthSession.actionCreators,
)(LoggedInRoute);

This gives me an error:

TypeScript error in /src/App.tsx(21,10): Type '{ path: string; component: ConnectedComponentClass<typeof Logout, Pick<AuthProps, "location" | "history" | "match" | "staticContext">>; }' is missing the following properties from type 'Readonly & ParamProps>': location, history, match, uuid, and 2 more. TS2740 19 | 20 | > 21 | | ^ 22 | 23 | );

I can clearly see in various documents that connect is designed to do what I want by injecting own properties and redux state into child control props, but clearly this isn't working for me.

How can I specify the connect such that I can pass redux state and also do the trick where you can pass through specific named properties and also the rest of the properties? - i.e. {stateprops, component, ...otherProps}

A long question but I'm sure that this is simple for most React programmers.

Thanks


Solution

  • I looked at this a little more and got it working:

    import * as React from "react";
    import { connect } from "react-redux";
    import { Route } from "react-router-dom";
    import { RouteComponentProps } from 'react-router';
    import { ApplicationState } from '../store';
    import * as AuthSession from '../store/AuthSession';
    
    type ParamProps = {
        exact?: boolean;
        path: string;
        component: React.ComponentType<any>;
        otherProps?: any
    };
    
    type ComponentProps =
        AuthSession.AuthState &
        typeof AuthSession.actionCreators &
        RouteComponentProps<{}> &
        ParamProps;
    
    
    class LoggedInRoute extends React.PureComponent<ComponentProps> {
        public render() {
            var props = this.props;
            var Component = props.component;
            if (this.props.IsAuthenticated === true) {
                return (
                    <React.Fragment>
                        <Route path={props.path}
                            render={otherProps => (
                                <>
                                    <Component component={props.component} {...otherProps} />
                                </>
                            )}
                        />
                    </React.Fragment>
                );
            } else {
                return (false);
    ;
            }
        }
    };
    
    export default connect(
        (state: ApplicationState, ownProps: any) => {
            const { component, path, ...rest } = ownProps;
            let op = {} as ParamProps;
            op.component = component;
            op.path = path;
            op.otherProps = rest;
            let st = { ...state.auth, ...op } as ComponentProps;
            return st;
            },
        AuthSession.actionCreators,
    )(LoggedInRoute);
    

    There were two changes required:

    1) Yes, you can compose your props in the connect state mapping. You only have to return the specific fields that you specified in your type (as expected). The injected "ownProps" property however does not come in with your specified type, you have to construct the correct type.

    2) When I got through this, it seems as though React didn't know how to render the component that was passed in. It turns out that JSX/TSX will render a component held in a variable if that variable begins with an upper case letter. So I assigned my "component" property into a "Component" variable. I really like this feature now that I understand it.

    :)