3
\$\begingroup\$

I have been creating a custom Tinder-like slider component for one of my work projects, and I have the majority of my logic inside of the index.js file. This has led to several functions being created making the component larger.

I was hoping too see if anyone has any suggestions on how to clean up this component and follow the best practices. I have been reading on the best practices regarding components and it has been suggested that components should be no longer than 150 lines of code.

Are there too many functions in my component?

It would be good to get other devs opinion on this.

import React, { useState, useRef, useEffect } from 'react';
import { Animated, PanResponder, Dimensions, StyleSheet, SafeAreaView, LayoutAnimation, UIManager, Text, View, Image } from 'react-native';
import BottomSwipeContainer from '../BottomSwipeContainer';
import NotLikedCardIcon from './NotLikedIcon';
import LikedCardIcon from './LikedCardIcon';

const SCREEN_WIDTH = Dimensions.get("window").width;
const SWIPE_THRESHOLD = 0.25 * SCREEN_WIDTH;
const SWIPE_OUT_DURATION = 250;

if (Platform.OS === 'android') {
    if (UIManager.setLayoutAnimationEnabledExperimental) {
        UIManager.setLayoutAnimationEnabledExperimental(true);
    }
}

export const Deck = (props) => {

    const pan = useRef(new Animated.ValueXY()).current;
    const [index, setIndex] = useState(0);

    useEffect(() => {
        if(props.data) { 
            setIndex(0);
        }
    }, [props.data]);

    const likeOpacity = pan.x.interpolate({
        inputRange: [-SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2],
        outputRange: [0, 0, 1],
        extrapolate: 'clamp'
    });

    const notOpacity = pan.x.interpolate({
        inputRange: [-SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2],
        outputRange: [1, 0, 0],
        extrapolate: 'clamp'
    });

    const panResponder = useRef(
        PanResponder.create({
            // position of item is started moving
            onStartShouldSetPanResponder: () => true,
            // callback function when the item is pressed/dragging
            onPanResponderMove: (event, gesture) => {
                // update the current position with gesture value
                pan.setValue({ x: gesture.dx, y: gesture.dy });
            },
            // called anytime a item has been released after interaction
            onPanResponderRelease: (event, gesture) => {
                // if card was dragged more distance then swipe threshold
                if(gesture.dx > SWIPE_THRESHOLD) {
                    forceSwipe('right');
                } else if(gesture.dx < -SWIPE_THRESHOLD) {
                    forceSwipe('left');
                } else {
                    resetPosition();
                }
            }
        })
    ).current;

    const getCardStyle = () => {
        
        const rotate = pan.x.interpolate({
            inputRange: [-SCREEN_WIDTH * 2, 0 ,SCREEN_WIDTH],
            outputRange: ['-120deg', '0deg', '120deg']
        });

        return {
            ...pan.getLayout(),
            transform: [{rotate}]
        }
    }

    const forceSwipe = (direction) => {
        const x = direction === 'right' ? SCREEN_WIDTH : -SCREEN_WIDTH;
        Animated.timing(
            pan,
            { 
                toValue: { x, y: 0 },
                duration: SWIPE_OUT_DURATION,
                useNativeDriver: false
            }
        // add callback function to the .start method
        // function will be called once the animation has been completed
        ).start(() => onSwipeComplete(direction));
    }

    const onSwipeComplete = (direction) => {
        const item = props.data[index];
        const { onSwipeLeft, onSwipeRight } = props;

        direction === 'right' ? onSwipeRight(item) : onSwipeLeft(item);
        // update position back to default
        pan.setValue({ x: 0, y: 0 });
        // update the index to the next element inside the array
        setIndex(index => (index + 1));
        // add layout animation effect after the next card is pushed up
        LayoutAnimation.configureNext(LayoutAnimation.Presets.spring);
    }
    
    const onSwipeBack = () => {
        // update the index to the next element inside the array
        pan.setValue({ x: 0, y: 0 });

        if(index !== 0) {
            setIndex(index => (index - 1));
        }

        LayoutAnimation.configureNext(LayoutAnimation.Presets.spring);

    }

    const resetPosition = () => {
        Animated.spring(
            pan,
            { 
                toValue: { x: 0, y: 0 },
                useNativeDriver: false
            }
        ).start();
    }

    const renderCards = () => {
        if(index >= props?.data?.length) {
            return props.renderNoMoreCards();
        }

        return props?.data?.map((item, i) => {
            if(i < index) { return null }; 
            
            if (i === index) {
                return (
                    <Animated.View
                        key={item?._id}
                        {...panResponder.panHandlers}
                        style={[getCardStyle()]}
                    >
                        {props.renderCard(item)}
                        <LikedCardIcon likeIconOpacity={likeOpacity} />
                        <NotLikedCardIcon dislikeIconOpacity={notOpacity} />
                    </Animated.View>
                );
            }
            
            return (
                <Animated.View key={item?._id} style={styles.cardStyle}>
                    {props.renderCard(item)} 
                </Animated.View>
            );
            
        }).reverse();
    }

    return (
        <SafeAreaView>
            <View style={styles.container}>
                {renderCards()}
            </View>

            {index != props?.data?.length &&
                <BottomSwipeContainer 
                    onSwipePrevious={onSwipeBack}
                    forceSwipe={forceSwipe}
                    task_id={props?.data[index]?._id}
                    distance={1}
                />
            }
        </SafeAreaView>
    );
}

const styles = StyleSheet.create({
    container: {
        marginTop: 25
    },
    cardStyle: {
        position: 'absolute',
        width: SCREEN_WIDTH,
    }
});

This is the Deck component that is passed in via the parent component for reference.

   <Deck
      data={data?.taskSearch}
      renderCard={renderCard}
      renderNoMoreCards={renderNoMoreCards}
      onSwipeRight={() => {}}
      onSwipeLeft={() => {}}
      forceSwipe={() => {}}
      onSwipePrevious={() => {}}
   />
New contributor
T_431 is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$

0

Your Answer

T_431 is a new contributor. Be nice, and check out our Code of Conduct.

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.