Joe Lloyd
Joe Lloyd

Reputation: 22503

How to extract common logic from a react component (But the common logic is using setState)

Background

I am building an app with react. I've been using react for 2.5 years now but this is the first time I'm really starting with tests. Because of this I'm removing as much logic from my components. Most of this is quite simple because it's easy to think in pure functions for most logic.

Problem

I have found myself reusing one particular method across different components because it's my handler for input fields. After I copy pasted it for the 3rd time I thought there must be a cleaner solution.

My function

This function currently exists inside 3 of my components in exactly the same way

/**
 * @description
 * Returns a function that updates the state form
 *
 * @param {String} stateKey - key in the state to update
 * @returns {Function}
 */
@autobind
updateInputValue (prop) {
    return (event) => {
        this.setState({ [prop]: event.target.value });
    }
};

What I have tried

I attempted to extract it out and pass this to it like a variable, it worked but I wondered if there was a cleaner way to do it.

/**
 * @description
 * Returns a function that updates the state of the filter form
 *
 * @param {Object} componentThis - The components context
 * @param {String} stateKey - key in the state to update
 * @returns {Function}
 */
function updateInputValue (componentThis, stateKey) {
    return (event) => {
        componentThis.setState({ [stateKey]: event.target.value });
    }
}

then in the input you're just doing

<Input id="foo"
       value={this.state.foo}
       onChange={updateInputValue(this, 'foo')} />

Something about passing this feels wrong (or at least open to something breaking), I wanted to know if there were other solutions for this problem?

Upvotes: 1

Views: 1007

Answers (2)

Tall Paul
Tall Paul

Reputation: 2450

So I actually have done this on a different project of mine. I treat it like there is a reducer from redux when setting state locally and a function that gets called, given the state and returns the entire state to set again.

This makes it extremely easy to test and reuse across different components. You can find this open sourced here

The project I am doing this on is a table, so if I wanted to go to the next set of data I would do the following.

(I did simplify this example a bit)

import { nextPage } from '../actions/tableActions'

nextPage() {
    this.setState(currentState => {
        return nextPage({ state: currentState })
    });
};

In my tableActions.js file it would look like

export const nextPage = ({ state }) => {
    const { currentPage } = state.pagination;

    return changePage({ ...state, currentPage: currentPage + 1 })
};

Now writing a test it is as simple as this.

it('should update the currentPage to the next page', () => {
    const given = {
        state: {
            pagination: {
                currentPage: 2,
            },
        }
    };
    const expected = {
        pagination: {
            currentPage: 3,
        },
    };

    expect(actions.nextPage(given)).toEqual(expected);
});

Upvotes: 2

Adeel Imran
Adeel Imran

Reputation: 13976

Approach #1

updateInputValue (prop, event) {
   return { [prop]: event.target.value };
};

In your code this

someFunction = (event) => {
   this.setState(updateInputValue('someFieldkey', event))
}

Approach #2

updateInputValue (theThis, prop, event) {
   theThis.setState({ [prop]: event.target.value });
};

In your code this

someFunction = (event) => {
   updateInputValue(this, 'someFieldkey', event))
}

For approach #2, I haven't tried this but since everything in javascript is by reference, when you pass this instance in that function and use it's setState method I think it should work.

Upvotes: 0

Related Questions