Mehedi Hassan Piash | Senior Software Engineer | Android | iOS | KMP | Ktor | Jetpack Compose | React-Native.

Showing posts with label javascript. Show all posts
Showing posts with label javascript. Show all posts

November 23, 2024

Zustand Simplified: Effortless State Management for Favorites in React Native

November 23, 2024 Posted by Piash , , , , No comments

 State management is a cornerstone of any React Native application. While libraries like Redux are popular, sometimes we need something lightweight yet powerful. Enter Zustand, a minimalist state management library that’s perfect for handling state in React Native. In this article, we’ll explore how to use Zustand to manage a favorites list for Movies and TV series in a React Native app.

zustand in react native

Why Use Zustand in React Native?
Zustand is simple, scalable, and performant. Here’s why it’s a great choice for React Native:
1. Minimal Boilerplate: Create stores without cumbersome setup.
2. Middleware Support: Includes built-in middleware for features like persistence.
3. Lightweight: Tiny bundle size and fast performance.
4. Reactivity: Automatically triggers re-renders when state changes.
5. Integration with MMKV Storage: Perfect for persisting data efficiently in React Native.

Setting Up Zustand in React Native
First, ensure you have the required packages installed:

npm install zustand
npm install react-native-mmkv zustand/middleware

Here’s how to set up a Zustand store for managing favorite Movies and TV series:
FavoriteStore.ts

import { create } from 'zustand';
import { MovieDetail } from '../types/MovieDetail.ts';
import { persist, createJSONStorage } from 'zustand/middleware';
import { TvSeriesDetail } from '../types/TvSeriesDetail.ts';
import { zustandMMKVStorage } from './mmkv.ts';

interface FavoriteMoviesTvSeriesStore {
favoriteMovies: MovieDetail[];
toggleFavoriteMovie: (movie: MovieDetail) => void;
isFavoriteMovie: (movieId: number) => boolean;
clearFavoriteMovies: () => void;
favoriteTvSeries: TvSeriesDetail[];
toggleFavoriteTvSeries: (movie: TvSeriesDetail) => void;
isFavoriteTvSeries: (tvSeriesId: number) => boolean;
clearFavoriteTvSeries: () => void;
}

export const useFavoriteStore = create<FavoriteMoviesTvSeriesStore>()(
persist(
(set, get) => ({
favoriteMovies: [],
toggleFavoriteMovie: (movie: MovieDetail) => {
set((state: FavoriteMoviesTvSeriesStore) => {
const isFav = state.favoriteMovies.some(
(favMovie: MovieDetail) => favMovie.id === movie.id
);
return {
favoriteMovies: isFav
? state.favoriteMovies.filter(
(favMovie: MovieDetail) => favMovie.id !== movie.id
) // Remove from favorites
: [...state.favoriteMovies, movie], // Add to favorites
};
});
},
isFavoriteMovie: (movieId: number) =>
get().favoriteMovies.some((movie: MovieDetail) => movie.id === movieId),

clearFavoriteMovies: () => set({ favoriteMovies: [] }),
favoriteTvSeries: [],
toggleFavoriteTvSeries: (tvSeries: TvSeriesDetail) => {
set((state: FavoriteMoviesTvSeriesStore) => {
const isFav = state.favoriteTvSeries.some(
(favTvSeries: TvSeriesDetail) => favTvSeries.id === tvSeries.id
);
return {
favoriteTvSeries: isFav
? state.favoriteTvSeries.filter(
(favTvSeries: TvSeriesDetail) =>
favTvSeries.id !== tvSeries.id
) // Remove from favorites
: [...state.favoriteTvSeries, tvSeries], // Add to favorites
};
});
},
isFavoriteTvSeries: (tvSeriesId: number) =>
get().favoriteTvSeries.some(
(tvSeries: TvSeriesDetail) => tvSeries.id === tvSeriesId
),
clearFavoriteTvSeries: () => set({ favoriteTvSeries: [] }),
}),
{
name: '@favorite',
storage: createJSONStorage(() => zustandMMKVStorage),
}
)
);

MMKV Integration: The zustandMMKVStorage integrates Zustand with react-native-mmkv, a high-performance storage solution. Here’s a simple MMKV wrapper:
MMKV Wrapper (mmkv.ts)

import { MMKV } from 'react-native-mmkv';

const mmkv = new MMKV();

export const zustandMMKVStorage = {
getItem: (name: string) => {
const value = mmkv.getString(name);
return value ? JSON.parse(value) : null;
},
setItem: (name: string, value: any) => {
mmkv.set(name, JSON.stringify(value));
},
removeItem: (name: string) => {
mmkv.delete(name);
},
};

Features Explained

  1. Add/Remove Favorite Movies or TV Series: Use toggleFavoriteMovie and toggleFavoriteTvSeries to add or remove items based on their presence in the list.
    2. Check if an Item is a Favorite: Use isFavoriteMovie or isFavoriteTvSeries to check whether a specific item is in the favorites list.
    3. Clear Favorites: Use clearFavoriteMovies and clearFavoriteTvSeries to reset the lists.
    4. Persistence with MMKV: The store is persisted using MMKV for fast, efficient local storage.

Using the Store in Components

import React, { useState } from 'react';
import { FlatList, View } from 'react-native';
import styles from './FavoriteMovie.style.ts';
import { useFavoriteStore } from '../../../local-store/FavoriteStore.ts';
import FavoriteComponent from '../../../components/favorite/FavoriteComponent.tsx';
import { MovieDetail } from '../../../types/MovieDetail.ts';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { RootStackParam } from '../../../types/navigation/NavigationTypes.ts';
import ConfirmationAlert from '../../../components/alert-dialog/ConfirmationAlert.tsx';
import { confirmationAlert } from '../../../constant/Dictionary.ts';

type FavoriteMovieNavigationProp = NavigationProp<
RootStackParam,
'MovieDetail'
>;

const FavoriteMovie = () => {
const navigation = useNavigation<FavoriteMovieNavigationProp>();
const { favoriteMovies, toggleFavoriteMovie } = useFavoriteStore();

const [visible, setVisible] = useState(false);
const [movieToRemove, setMovieToRemove] = useState<MovieDetail | null>(null);

const showDialog = (movie: MovieDetail) => {
setMovieToRemove(movie);
setVisible(true);
};

const hideDialog = () => {
setVisible(false);
setMovieToRemove(null);
};

const confirmRemoveFavorite = () => {
if (movieToRemove) {
toggleFavoriteMovie(movieToRemove);
}
hideDialog();
};

const favorite = ({ item }: { item: MovieDetail }) => (
<FavoriteComponent
title={item.title}
poster_path={item.poster_path}
onPress={() => navigation.navigate('MovieDetail', { movieId: item.id })}
onRemove={() => showDialog(item)}
/>
);

return (
<View style={styles.mainView}>
<FlatList
style={styles.flatListStyle}
data={favoriteMovies}
renderItem={favorite}
keyExtractor={(item) => item.id.toString()} // Ensure a unique key for each item
/>
<ConfirmationAlert
visible={visible}
title={confirmationAlert.title}
message={confirmationAlert.message}
onConfirm={confirmRemoveFavorite}
onCancel={hideDialog}
/>
</View>
);
};

export default FavoriteMovie;

GitHub link: https://github.com/piashcse/react-native-movie
Medium: https://piashcse.medium.com/zustand-simplified-effortless-state-management-for-favorites-in-react-native-0b51af8b323a

October 20, 2024

ESLint and Prettier configuration for React-Native and Expo

October 20, 2024 Posted by Piash , , , , No comments

 Setting up ESLint and Prettier in your React Native or Expo project is essential for maintaining consistent code quality and style. ESLint helps identify and fix problematic code patterns, while Prettier ensures your code is formatted consistently across the entire team. In this guide, we’ll walk through configuring ESLint and Prettier for a React Native/Expo project step-by-step.

Step 1: Create a New React Native or Expo Project

npx react-native init expo-museum 
npx create-expo-app expo-museum

Navigate to your project directory:

cd expo-museum

Step 2: Install ESLint and Prettier
Remove the previous ESLint configuration:

npm uninstall @react-native-community/eslint-config eslint

Add ESLint and Prettier as development dependencies:

npm install eslint prettier --save-dev

Additionally, you’ll need some ESLint plugins for React Native and Prettier integration:

npm install eslint-config-prettier eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-native - save-dev

Step 3: Initialize ESLint
Run the following command to initialize ESLint and generate a configuration file:

npx eslint - init

Follow the prompts as outlined below:

Step 4: Add ESLint configuration
Add the given configuration to the newly created eslint.config.js

import globals from 'globals';
import pluginJs from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginReact from 'eslint-plugin-react';
import prettierConfig from 'eslint-config-prettier';
import prettierPlugin from 'eslint-plugin-prettier';

export default [
{ files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'] },
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
prettierConfig, // Disables conflicting ESLint rules with Prettier
{
ignores: ['node_modules/', 'android/', 'ios/', 'build/'],
plugins: { prettier: prettierPlugin }, // Register the Prettier plugin
rules: {
'react/react-in-jsx-scope': 'off',
'no-unused-vars': 'warn',
'react-native/no-inline-styles': 'off',
'prettier/prettier': 'error', // Enforce Prettier rules
},
settings: {
react: {
version: 'detect', // Automatically detect React version
},
},
},
];

Step 5: Create Prettier Configuration
Create a .prettierrc file in the root of your project:

{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 80,
"tabWidth": 2,
"bracketSpacing": true,
"jsxBracketSameLine": false,
"useTabs": false
}

Create a .prettierignore file in the root of your project:

node_modules/
android/
ios/
build/

Step 6: Add Scripts for ESLint and Prettier
Update your package.json with scripts to run ESLint and Prettier:

"lint": "npx eslint .",
"lintFixAll": "npx eslint '**/*.{js,jsx,ts,tsx}' --fix",
"prettierFixAll": "npx prettier --write '**/*.{js,jsx,ts,tsx,json,md}'",
"fix:lintPrettier": "npm run prettierFixAll && npm run lintFixAll"

Step 7: Test your configuration by running the script in your terminal

npm run lint 
npm run lintFixAll
npm run prettierFixAll
npm run fix:lintPrettier

Here is the result you will get and change your configuration according to your needs 🙂 Happy coding.

GitHub Link: https://github.com/piashcse/expo-museum
Medium: https://piashcse.medium.com/eslint-and-prettier-configuration-for-react-native-and-expo-cbe333bd3421

May 27, 2022

Modern redux architecture with redux-saga and redux-toolkit

May 27, 2022 Posted by Piash , No comments

 Redux is the most popular library for state management in react and react-native app development. On the other redux-saga is a middleware library used to allow a Redux store to interact with resources outside of itself asynchronously. So it's almost common to use redux-saga with redux.

import {createSlice} from '@reduxjs/toolkit'

const movieDetailState = createSlice({
name: 'movieDetail', initialState: {
movieDetail: {}, isLoading: false,
}, reducers: {
getMovieDetail: (state, action) => {
state.isLoading = true
}, movieDetailSuccess: (state, action) => {
state.movieDetail = action.payload;
state.isLoading = false
}, movieDetailFailure: (state) => {
state.isLoading = false
}
}
});
export const {getMovieDetail, movieDetailSuccess, movieDetailFailure} = movieDetailState.actions;
export default movieDetailState.reducer
import {configureStore} from '@reduxjs/toolkit'
import combineReducers from './reducer';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas';
import logger from 'redux-logger';

const sagaMiddleware = createSagaMiddleware();
const middleware = [sagaMiddleware];

const configurationAppStore = () => {
const store = configureStore({
reducer: combineReducers, middleware: [...middleware, logger], devTools: process.env.NODE_ENV === 'development'
})
sagaMiddleware.run(rootSaga);
return store
}
export default configurationAppStore
import {takeEvery, call, put} from 'redux-saga/effects';
import AxiosService from '../../../networks/AxiosService';
import {ApiUrls} from '../../../networks/ApiUrls';
import {movieDetailSuccess, movieDetailFailure, getMovieDetail} from './../../reducer/moviedetail'
function* movieDetailApi(action) {
try {
const response = yield call(AxiosService.getServiceData, ApiUrls.MOVIE_DETAIL(action.payload.movieId), {});
const result = response.data;
yield put(movieDetailSuccess(result));
} catch (error) {
yield put(movieDetailFailure());
}
}
const combineSagas = [takeEvery(takeEvery(getMovieDetail.type, movieDetailApi)];
export default combineSagas
import {all} from 'redux-saga/effects';
import combineSagas from "./movielist";

export default function* rootSaga() {
yield all([...combineSagas]);
}
import React from 'react';
import configureStore from './src/redux';
import {Provider} from 'react-redux';
import Navigation from './src/navigation/AppNavigation';

const store = configureStore();
const App = () => {
return (
<Provider store={store}>
<Navigation />
</Provider>
);
};

export default App;
import React, {useEffect} from 'react';
import {useSelector, useDispatch} from 'react-redux';
import Loading from '../../components/loading/Loading';
import styles from './MovieDetailStyle'
import {FlatList, Image, Text, TouchableOpacity, View, ScrollView} from "react-native";
import {Constants} from "../../appconstants/AppConstants";
import {getMovieDetail} from './../../redux/reducer/moviedetail';

const MovieDetail = ({navigation, route}) => {
const {movieId} = route.params
//communicate with redux
const {isLoading, movieDetail} = useSelector(state => state.movieDetailReducer);

// Api call
useEffect(() => {
dispatch(getMovieDetail({movieId}))
}, [])

// main view with loading while api call is going on
return isLoading ? <Loading/> : (<ScrollView style={styles.mainView}>
<Image
style={styles.imageView}
source={{
uri: `${Constants.IMAGE_URL}${movieDetail?.poster_path}`,
}}/>
<View style={styles.secondContainer}>
<Text style={styles.title}>{movieDetail.title}</Text>
<View style={styles.thirdContainer}>
<View style={styles.fourthContainer}>
<Text style={styles.infoTitleData}>{movieDetail.original_language}</Text>
<Text style={styles.infoTitle}>Language</Text>
</View>
<View style={styles.fourthContainer}>
<Text style={styles.infoTitleData}>{movieDetail.vote_average}</Text>
<Text style={styles.infoTitle}>Rating</Text>
</View>
<View style={styles.fourthContainer}>
<Text style={styles.infoTitleData}>{movieDetail.runtime} min</Text>
<Text style={styles.infoTitle}>Duration</Text>
</View>
<View style={styles.fourthContainer}>
<Text style={styles.infoTitleData}>{movieDetail.release_date}</Text>
<Text style={styles.infoTitle}>Release Date</Text>
</View>
</View>
<Text style={styles.description}>Description</Text>
<Text>{movieDetail.overview}</Text>
<Text style={styles.description}>Similar</Text>
</View>
</ScrollView>)
}
export default MovieDetail